这个问题是关于如何在现代 C++ 值类型中实现赋值,目的是避免误导性代码或与内置行为不一致的代码。
在 C++ 中,内置类型的生成值(右值?)是不可赋值的:
int generator_double() {
return 50;
}
...
generator_double() = 60; // error: expression is not assignable
这可能是一件好事,因为它不会给人一种可以用返回值做某事的错觉。
然而,天真的设计的用户类并不遵循这个规则。
struct UserClass {
int val_;
};
UserClass generator_user_class() {
return UserClass{99};
}
...
generator_user_class1() = UserClass{66}; // compiles
这可以工作并编译,这可能会产生误导。 我想这是允许的,因为
operator=
可能会产生不会被丢弃的副作用。
或者也许有人想将此分配的值传递给第三个函数
fun(generator_user_class1() = UserClass{66});
,尽管对于纯值,这应该与fun(UserClass{66});
相同。
因此,这激发了至少 3 或 4 种不同的方法来实现分配,并逐步改进可能不一致的初始版本:
struct UserClass1 {
int val_;
UserClass1& operator=(UserClass1 const& other) & = default;
UserClass1& operator=(UserClass1 const& other) && = default;
};
UserClass1 generator_user_class1() { return UserClass1{99}; }
...
generator_user_class1() = UserClass1{66}; // compiles, can be misleading
struct UserClass2 {
int val_;
UserClass2& operator=(UserClass2 const& other) & = default;
UserClass2&& operator=(UserClass2 const& other) && = delete;
};
UserClass2 generator_user_class2() { return UserClass2{99}; }
...
generator_user_class1() = UserClass1{66};
struct UserClass3 {
int val_;
UserClass3& operator=(UserClass3 const& other) & = default;
[[nodiscard]] UserClass3&& operator=(UserClass3 const& other) && {
return std::move(operator=(other));
}
};
UserClass3 generator_user_class3() { return UserClass3{99}; }
...
generator_user_class3() = UserClass3{66}; // warns because of [[nodiscard]]
[[nodiscard]]
并使所有这些自动工作,但这没有帮助,它只是迫使我实现operator= &&
返回值。这里是 Godbolt 中可玩的所有选项。 https://godbolt.org/z/h68hrxY19
那么,执行作业的正确现代方法是什么? 目前还有其他习语可以处理这个问题吗?
(为简单起见,讨论可以仅限于语义值的类型。)
执行任务的正确现代方法是什么?
大多数现代指南都认为正确的方法是忽略分配给右值的问题。 核心准则说:
C.20:如果可以避免定义默认操作,请执行 <...> 注意 此操作 被称为“零法则”。
Cppreference 说:
零法则
具有自定义析构函数、复制/移动的类 构造函数或复制/移动赋值运算符应该专门处理 具有所有权(源自单一责任 原则)。其他类不应该有自定义析构函数, 复制/移动构造函数或复制/移动赋值运算符。
但是假设您仍然想捕获“右值赋值”错误。让我们考虑一下您的示例,但从
std::vector<int> val_;
成员而不是 int val;
开始。即,从以下开始:
struct UserClass {
int val_;
};
然后,解决问题的第一种方法失去了移动语义:
struct UserClass2 {
std::vector<int> val_;
UserClass2& operator=(UserClass2 const& other) & = default;
UserClass2& operator=(UserClass2 const& other) && = delete;
};
问题在于,通过定义复制赋值运算符,您抑制了移动构造函数和移动赋值。您必须添加它们:
struct UserClass2 {
std::vector<int> val_;
UserClass2& operator=(UserClass2 const& other) & = default;
UserClass2& operator=(UserClass2&& other) & = default;
UserClass2(UserClass2&& other) = default;
};
请注意,您不需要删除任何赋值运算符:它们不会生成,因为您还有其他赋值运算符。我可能会遵循“5 规则”并添加其他 2 个运算符,即使它们不是严格需要的:
struct UserClass2 {
std::vector<int> val_;
UserClass2& operator=(UserClass2 const& other) & = default;
UserClass2& operator=(UserClass2&& other) & = default;
UserClass2(UserClass2& other) = default;
UserClass2(UserClass2&& other) = default;
~UserClass2() = default;
};
现在,你的
UserClass3
示例怎么样?您可以再添加 2 个 [[nodiscard]]
运算符来进行复制分配和移动分配,但是,在我看来,添加这么多代码只是为了将某些内容标记为“不丢弃”是不值得的。