我经常需要为“原始”资源句柄(如文件句柄、Win32 OS 句柄等)实现 C++ 包装器。这样做时,我还需要实现移动运算符,因为默认的编译器生成的不会清除移动的对象,从而产生双重删除问题。
在实现移动赋值运算符时,我更喜欢显式调用析构函数并使用placement new 就地重新创建对象。这样,我就避免了重复析构逻辑。此外,我经常以复制+移动(相关时)的方式实现复制分配。这导致以下代码:
/** Canonical move-assignment operator.
Assumes no const or reference members. */
TYPE& operator = (TYPE && other) noexcept {
if (&other == this)
return *this; // self-assign
static_assert(std::is_final<TYPE>::value, "class must be final");
static_assert(noexcept(this->~TYPE()), "dtor must be noexcept");
this->~TYPE();
static_assert(noexcept(TYPE(std::move(other))), "move-ctor must be noexcept");
new(this) TYPE(std::move(other));
return *this;
}
/** Canonical copy-assignment operator. */
TYPE& operator = (const TYPE& other) {
if (&other == this)
return *this; // self-assign
TYPE copy(other); // may throw
static_assert(noexcept(operator = (std::move(copy))), "move-assignment must be noexcept");
operator = (std::move(copy));
return *this;
}
这让我觉得很奇怪,但我没有在网上看到任何关于以这种“规范”方式实现移动+复制分配运算符的建议。相反,大多数网站倾向于以特定类型的方式实现赋值运算符,在维护类时必须手动与构造函数和析构函数保持同步。
是否有反对以这种与类型无关的“规范”方式实现移动和复制赋值运算符的论点(除了性能)?
更新 2019-10-08 基于 UB 评论:
我已经阅读了http://eel.is/c++draft/basic.life#8似乎涵盖了相关案例。提炼:
如果,在对象的生命周期结束后......,在原始对象占用的存储位置创建一个新对象,一个指向原始对象的指针,一个指向原始对象的引用,......将自动引用新对象,并且,...,可用于操作新对象,如果...
此后有一些明显的条件与相同的类型和 const/reference 成员相关,但它们似乎是任何赋值运算符实现所必需的。如果我错了,请纠正我,但这在我看来就像我的“规范”样本表现良好而不是UB(?)
基于复制和交换评论的更新 2019-10-10:
赋值实现可以合并到一个方法中,该方法采用值参数而不是引用。这似乎也消除了对 static_assert 和自分配检查的需要。然后我提出的新实现变为:
/** Canonical copy/move-assignment operator.
Assumes no const or reference members. */
TYPE& operator = (TYPE other) noexcept {
static_assert(!std::has_virtual_destructor<TYPE>::value, "dtor cannot be virtual");
this->~TYPE();
new(this) TYPE(std::move(other));
return *this;
}