构造函数初始化列表中的约束检查

时间:2014-05-23 13:56:20

标签: c++ initialization constraints initialization-list

这种情况与How to make a constraint on the parameters of the constructor有关,但情况略有不同。

您想要初始化非默认构造成员,但需要在构造之前检查约束。

实施例

(请注意,这只是一个例子。在这种特定情况下是否应该使用无符号整数是可以讨论的,但问题实际上是关于你想检查构造函数的一般情况)

您有以下课程:

class Buffer {
public:
    Buffer() = delete;
    Buffer(int size) noexcept;
};
....


class RenderTarget {
public:
    ....
private:
    int width_, height_;
    Buffer surface_;
};

构造函数必须检查整数参数的有效性:

RenderTarget::RenderTarget(int width, int height) :
    width_(width), height_(height),
    surface_(width_*height)
{
    if (width_<0 || height_<0)
        throw std::logic_error("Crizzle id boom shackalack");
}

注意Buffer没有默认构造函数,真正的构造函数是noexcept,即没有办法捕获错误。

当整数参数为负数时,已经有一个surface_。使用约束值在之前进行约束检查会更好。有可能吗?

3 个答案:

答案 0 :(得分:16)

命名构造函数

您可以使用所谓的命名构造函数(另请参阅http://www.parashift.com/c++-faq/named-ctor-idiom.html),并构建构造函数private

class RenderTarget {
private:
    RenderTarget (int w, int h) :
        width_(w), height_(h), buffer_(w*h) 
    {
        // NOTE: Error checking completely removed.
    }

public:
    static RenderTarget create(int width, int height) {
        // Constraint Checking
        if (width<0 || height<0)
            throw std::logic_error("Crizzle id boom shackalack");

        return RenderTarget(width, height);
    }

如果您有多个可能不明确使用的构造函数,则命名构造函数很有用,例如: 温度&lt; - 摄氏度|华氏温度|开尔文距离&lt; - Meter |院子里| Cubit |公里|。公里...

否则,(个人意见)他们会施加意想不到的抽象,也会分散注意力,应该避免。

三元运营商和throw

C ++允许 [expr.cond] 在三元运算符(throw - 运算符)的一个或两个操作数中使用?: - 表达式:

RenderTarget(int w, int h) :
    width_(w<0 ? throw std::logic_error("Crizzle id boom shackalack") : w),
    height_(h<0 ? throw std::logic_error("Crizzle id boom shackalack") : h),
    surface_(w*h)
{}

如果你不存储参数,你当然也可以在表达式中使用?:

RenderTarget(int w, int h) :
    surface_(
       (w<0 ? throw std::logic_error("Crizzle id boom shackalack") : w)
     * (h<0 ? throw std::logic_error("Crizzle id boom shackalack") : h)
    )
{}

或者您将前置条件检查合并为一个操作数:

RenderTarget(int w, int h) :
    surface_(
       (w<0||h<0) ? throw std::logic_error("Crizzle id boom shackalack") :
       w * h
    )
{}

?: - 运算符与throw - 表达式内联一起使用可以非常适合基本约束检查,并避免不得不回退使用默认构造函数(如果任何),然后在构造函数体中进行“实际初始化”。

对于更复杂的情况,这可能会变得有点笨拙。

静态私人会员

当然,可以使用两全其美:

private:
    static bool check_preconditions(int width, int height) {
        if (width<0 || height<0)
            return false;
        return true;
    }
public:
    RenderTarget(int w, int h) :
        surface_(
           check_preconditions(w,h) ?
           w*h :
           throw std::logic_error("Crizzle id boom shackalack")
        )
    {}

...或者您为需要预处理检查的任何成员编写静态函数:

private:
    static Buffer create_surface(int width, int height) {
        if (width<0 || height<0)
            throw std::logic_error("Crizzle id boom shackalack")
        return Buffer(width*height);
    }

public:
    RenderTarget(int w, int h) :
      surface_(create_surface(w, h))
    {}

这很好,因为您拥有完整的C ++ - 用于约束检查的机器,例如可以轻松添加日志记录。它可以很好地扩展,但对于简单的场景来说不那么方便。

答案 1 :(得分:3)

带有内联throw

phresnel’s solutionCurg’s answer中使用无符号整数的建议都暗示了一般解决方案:使用类型来确保构造值正确。

如果宽度和高度不能为负,那么使它们无符号可能是一个不错的选择 - 但如果存在最大界限,则可能需要更精确的类型来指定不变量,例如:

template<class T, T min, T max>
struct ranged {
  ranged(const T v)
    : value_(v < min || v > max ? throw range_error("...") : v) {}
  const T value_;
};

然后你可能会说:

ranged<unsigned int, 0, 1600> width_;
ranged<unsigned int, 0, 1200> height_;

但是,您可能希望强制宽度和高度的宽高比不大于16:9。因此,您可以将它们捆绑为Size类型,依此类推。这样,RenderTarget成员上的所有验证逻辑都是在构造函数体开始时完成的。

这种封装是面向对象编程的基础:对象的公共接口不能用于将其置于无效状态,构造函数是公共接口的一部分。

答案 2 :(得分:2)

还可以通过使尺寸,高度和宽度无符号来简化问题,这将防止进入负面状态。

class Buffer {
public:
    Buffer() = delete;
    Buffer(unsigned int size) noexcept;
};
....


class RenderTarget {
public:
    ....
private:
    unsigned int width_, height_;
    Buffer surface_;
};

构造函数必须检查整数参数的有效性:

RenderTarget::RenderTarget(unsigned int width, unsigned int height) :
    width_(width), height_(height),
    surface_(width_*height)
{
    // never a need to throw on negative values...
}

其他错误处理方法:

如果使用类型来限制无效值是不够的,那么除了抛出异常之外,还有许多经过尝试和测试的方法来处理错误情况,例如:

Buffer(int size, bool& success) {}

class Buffer {
...
    bool isValid()
};

template<typename T>
struct ValidatedValue
{
    ValidatedValue(T value, T min, T max)
        : _value(value)
        , _isValid(value >= min && value <= max)
    {
    }

    bool isValid() const { return _isValid; }
private:
    T _value;
    bool _isValid;
};

或......

许多其他选择。

每种验证数据的方法都有利有弊,但我通常建议保持解决方案的简单性,使其易于维护和可读,因为这些解决方案通常都可能过度设计。