10

这种情况与如何对构造函数的参数进行约束有关,但略有不同。

您想要初始化一个不可默认构造的成员,但需要在构造它之前检查约束。

例子:

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

您有以下课程:

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");
}

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

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

4

3 回答 3

17

命名构造函数

您可以使用所谓的命名构造函数(另请参阅https://isocpp.org/wiki/faq/ctors#named-ctor-idiom),并制作构造函数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);
    }

如果您有多个使用起来可能不明确的构造函数,命名构造函数会很有趣,例如Temperature <-- Celsius | 华氏度 | 开尔文距离 <-- 米 | 院子 | 肘 | 公里 | ... .

否则,(个人意见)它们会带来意想不到的抽象和分心,应该避免。

三元运算符和throw

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

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
    )
{}

?:-operator 与throw-expression内联使用对于基本的约束检查非常好,并且避免不得不回退到使用默认构造函数(如果有的话),然后在构造函数体内进行“真正的初始化”。

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

静态私有成员

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

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++ 机器来进行约束检查,例如可以轻松添加日志记录。它可以很好地扩展,但对于简单的场景来说不太方便。

于 2014-05-23T13:56:20.337 回答
3

phresnel 的内联解决方案和Curg 的使用无符号整数的答案throw中的建议都暗示了这里的一般解决方案:使用类型来确保值通过构造是正确的。

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

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都已完成。

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

于 2014-05-25T07:28:20.907 回答
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;
};

或者 ...

许多其他选择。

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

于 2014-05-23T14:09:28.713 回答