这种情况与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_
。使用约束值在之前进行约束检查会更好。有可能吗?
答案 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 solution和Curg’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;
};
或......
许多其他选择。
每种验证数据的方法都有利有弊,但我通常建议保持解决方案的简单性,使其易于维护和可读,因为这些解决方案通常都可能过度设计。