现代C++值类型赋值运算符的正确实现方式探讨
值类型的核心设计目标之一是对齐内置类型的行为,而内置类型的右值(如临时对象、std::move后的对象)无法作为赋值操作的左操作数(例如int() = 42;会触发编译错误)。用户自定义值类型默认允许右值赋值,容易让开发者误以为这种操作有实际意义(实际上临时对象赋值后会立即销毁,毫无作用),因此正确的实现需要对齐内置类型的语义。
一、推荐的标准实现:限制赋值运算符仅用于左值
通过成员函数引用限定符(C++11引入),让赋值运算符只能被左值对象调用,直接禁止右值赋值的编译可能性,完全对齐内置类型行为:
class ValueType { public: // 拷贝赋值:仅左值可调用 ValueType& operator=(const ValueType&) & = default; // 移动赋值:仅左值可调用(保留合法的移动语义) ValueType& operator=(ValueType&&) & = default; // 其他必要成员(构造函数等) ValueType() = default; ValueType(const ValueType&) = default; ValueType(ValueType&&) = default; ~ValueType() = default; };
效果
- 合法操作:
ValueType a, b; a = b; a = std::move(b);(左值对象接收拷贝/移动值) - 非法操作:
ValueType{} = a; std::move(a) = b;(右值作为左操作数,编译错误)
这种方式既保留了必要的拷贝/移动语义,又彻底避免了无意义的右值赋值操作,是值类型赋值运算符的首选实现。
二、兼容场景:标记返回值不可丢弃
如果出于历史兼容或特殊需求,无法完全禁止右值赋值,可以使用C++17的[[nodiscard]]属性标记赋值运算符的返回值,让编译器对无意义的赋值操作发出警告:
class ValueType { public: [[nodiscard]] ValueType& operator=(const ValueType&) = default; [[nodiscard]] ValueType& operator=(ValueType&&) = default; // 其他成员... };
效果
ValueType{} = a;这类操作不会编译错误,但编译器会发出类似“丢弃nodiscard值”的警告,提醒开发者该操作无实际意义。- 适合需要兼容旧代码,但希望引导开发者避免不良写法的场景。
三、极端场景:完全禁用移动赋值(不推荐)
在某些不需要移动语义的场景下,可以显式删除移动赋值运算符,但这种做法仅适用于完全不需要移动的类型,会牺牲性能,因此不推荐作为通用值类型的实现:
class ValueType { public: ValueType& operator=(const ValueType&) & = default; // 禁用移动赋值 ValueType& operator=(ValueType&&) = delete; // 其他成员... };
注意
这种写法会禁止a = std::move(b);这类合法的移动操作,仅适合确实不需要移动语义的特殊类型。
其他相关惯用写法
1. 链式赋值的一致性
无论采用哪种实现,赋值运算符都应该返回*this的左值引用(ValueType&),保持和内置类型一致的链式赋值语义(如a = b = c;),这是C++的通用惯例。
2. 结合值类型的不可变性
如果你的值类型是不可变类型(类似std::string_view或整数类型,构造后无法修改),则应该完全禁用赋值运算符:
class ImmutableValue { public: // 禁用所有赋值 ImmutableValue& operator=(const ImmutableValue&) = delete; ImmutableValue& operator=(ImmutableValue&&) = delete; // 仅提供构造函数 explicit ImmutableValue(int val) : value(val) {} private: int value; };
3. 自定义赋值逻辑的安全实现
如果需要自定义赋值逻辑(而非使用default),要确保异常安全性(遵循强异常安全保证),同时保留左值引用限定符:
class ValueType { public: ValueType& operator=(const ValueType& other) & { if (this != &other) { // 先构造临时对象,再交换(强异常安全) ValueType temp(other); swap(temp); } return *this; } ValueType& operator=(ValueType&& other) & noexcept { swap(other); return *this; } void swap(ValueType& other) noexcept { using std::swap; swap(member, other.member); // 交换其他成员... } private: int member; };
这种“拷贝并交换”(copy-and-swap)是自定义赋值运算符的惯用写法,既保证异常安全,又简化了代码逻辑。
内容的提问来源于stack exchange,提问作者alfC

