由于到目前为止我觉得 Stepanov 在答案中被歪曲了,所以让我快速概述一下我自己的观点:
对于std 类型(以及仅那些),标准规定移出对象处于著名的“有效但未指定”状态。特别是,std 类型都没有使用 Stepanov 的部分形成状态,包括我在内的一些人认为这是一个错误。
对于您的自己的类型,您应该争取默认构造函数以及移动的源对象以建立部分形成状态,Stepanov 在 Elements of Programming (2009) 中定义作为一种状态,其中唯一有效的操作是销毁和分配新值。特别是,Partially-Formed State 不需要表示对象的有效值,也不需要遵守正常的类不变量。
与流行的看法相反,这并不是什么新鲜事。 Partially-Formed State 自 C/C++ 诞生之日起就存在:
int i; // i is Partially-Formed: only going out of scope and
// assignment are allowed, and compilers understand this!
这对用户来说实际上意味着永远不要假设您可以对已移动的对象做更多的事情,而不是销毁它或为它分配一个新值,当然,除非文档说明您可以做更多,这对于容器来说通常是可能的,它们通常可以自然而有效地建立空状态。
对于班级作者,这意味着您有两种选择:
首先,您要避免像 STL 那样的部分形成状态。但是对于具有远程状态的类,例如一个 pimpl'ed 类,这意味着要表示一个有效值,要么接受 nullptr 作为 pImpl 的有效值,提示您在公共 API 级别定义 nullptr pImpl 的含义, 包括在所有成员函数中检查nullptr。
或者您需要为移动的(和默认构造的)对象分配一个新的pImpl,这当然不是任何注重性能的 C++ 程序员会做的事情。然而,一个注重性能的 C++ 程序员也不希望在他的代码中乱扔nullptr 检查,只是为了支持一个不平凡的使用移出对象的次要用例。
这将我们带到了第二种选择:拥抱部分形成的国家。这意味着,您接受 nullptr pImpl,但仅适用于默认构造和移出的对象。 nullptr pImpl 代表部分形成状态,其中只允许销毁和分配另一个值。这意味着只有 dtor 和赋值运算符需要能够处理nullptr pImpl,而所有其他成员都可以假定一个有效的pImpl。这还有另一个好处:您的默认 ctor 和移动运算符都可以是 noexcept,这对于在 std::vector 中使用很重要(因此在重新分配时使用移动而不是副本)。
例如Pen类:
class Pen {
struct Private;
Private *pImpl = nullptr;
public:
Pen() noexcept = default;
Pen(Pen &&other) noexcept : pImpl{std::exchange(other.pImpl, {})} {}
Pen(const Pen &other) : pImpl{new Private{*other.pImpl}} {} // assumes valid `other`
Pen &operator=(Pen &&other) noexcept {
Pen(std::move(other)).swap(*this);
return *this;
}
Pen &operator=(const Pen &other) {
Pen(other).swap(*this);
return *this;
}
void swap(Pen &other) noexcept {
using std::swap;
swap(pImpl, other.pImpl);
}
int width() const { return pImpl->width; }
// ...
};