很久以前,当我学习C ++时,我就强烈强调C ++的部分意思是就像循环具有“循环不变式”一样,类也具有与对象生命周期相关的不变式-应该是正确的只要物体还活着 应该由构造函数建立并由方法保留的事物。封装/访问控制可帮助您强制执行不变式。RAII是您可以使用此想法做的一件事。
从C ++ 11开始,我们现在有了移动语义。对于支持移动的类,从某个对象移动不会正式终止其生命周期,因为该移动应该使它处于某种“有效”状态。
在设计一个类时,如果将其设计为仅保留该类的不变性直到将其移出之前,是不好的做法吗?或者是说没关系,如果它可以让你让它走得更快。
具体来说,假设我有一个不可复制但可移动的资源类型,如下所示:
class opaque {
opaque(const opaque &) = delete;
public:
opaque(opaque &&);
...
void mysterious();
void mysterious(int);
void mysterious(std::vector<std::string>);
};
无论出于何种原因,我都需要为此对象创建一个可复制的包装,以便可以在某些现有的调度系统中使用它。
class copyable_opaque {
std::shared_ptr<opaque> o_;
copyable_opaque() = delete;
public:
explicit copyable_opaque(opaque _o)
: o_(std::make_shared<opaque>(std::move(_o)))
{}
void operator()() { o_->mysterious(); }
void operator()(int i) { o_->mysterious(i); }
void operator()(std::vector<std::string> v) { o_->mysterious(v); }
};
在此copyable_opaque
对象中,构造时建立的类的不变性是成员o_
始终指向有效对象,因为没有默认的ctor,并且唯一不是副本ctor的ctor保证了这些。所有operator()
方法都假定此不变式成立,并随后将其保留。
但是,如果将对象从移出,o_
则将指向无物。在那之后,调用任何方法operator()
都将导致UB /崩溃。
如果从不移动对象,则将一直保持不变,直到dtor调用为止。
假设,我写了这个课,几个月后,我虚构的同事经历了UB,因为在某种复杂的功能中,由于某些原因,许多这些对象被拖曳了,他从其中之一移走了,后来称为其中之一。它的方法。显然,这是他一天结束时的错,但是这门课“设计得不好吗?”
想法:
在C ++中创建僵尸对象通常是不好的形式,如果您触摸它们就会爆炸。
如果无法构造某些对象,则无法建立不变式,然后从ctor引发异常。如果您无法以某种方法保留不变式,则以某种方式发出错误信号并回滚。对于移出的对象这应该有所不同吗?在标头中仅记录“从该对象移出之后,除销毁该对象之外,对其进行任何其他操作都是非法的”(UB)是否足够?
在每个方法调用中不断断言它是否有效更好?
像这样:
class copyable_opaque {
std::shared_ptr<opaque> o_;
copyable_opaque() = delete;
public:
explicit copyable_opaque(opaque _o)
: o_(std::make_shared<opaque>(std::move(_o)))
{}
void operator()() { assert(o_); o_->mysterious(); }
void operator()(int i) { assert(o_); o_->mysterious(i); }
void operator()(std::vector<std::string> v) { assert(o_); o_->mysterious(v); }
};
这些断言并不能从根本上改善行为,并且会导致速度变慢。如果您的项目确实使用“发布版本/调试版本”方案,而不是仅始终使用断言运行,我想这会更具吸引力,因为您无需为发布版本中的检查付费。如果您实际上没有调试版本,那么这似乎就没有吸引力。
- 使该类具有可复制性而不是可移动性更好吗?
这似乎也很糟糕,并且会导致性能下降,但是它可以直接解决“不变”问题。
您认为这里的相关“最佳做法”是什么?