实现值类型赋值的现代方法

问题描述 投票:0回答:1

这个问题是关于如何在现代 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 种不同的方法来实现分配,并逐步改进可能不一致的初始版本:

  1. 这是默认行为:
 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
  1. 禁用 r 值分配
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};
  1. 允许对右值进行赋值,如 1),但将返回标记为不丢弃。
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]]
  1. 让它工作的第四个选择是将整个类声明为
    [[nodiscard]]
    并使所有这些自动工作,但这没有帮助,它只是迫使我实现
    operator= &&
    返回值。

这里是 Godbolt 中可玩的所有选项。 https://godbolt.org/z/h68hrxY19

那么,执行作业的正确现代方法是什么? 目前还有其他习语可以处理这个问题吗?

(为简单起见,讨论可以仅限于语义值的类型。)

c++ move-semantics rvalue-reference copy-assignment
1个回答
0
投票

执行任务的正确现代方法是什么?

大多数现代指南都认为正确的方法是忽略分配给右值的问题。 核心准则说:

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]]
运算符来进行复制分配和移动分配,但是,在我看来,添加这么多代码只是为了将某些内容标记为“不丢弃”是不值得的。

最新问题
© www.soinside.com 2019 - 2025. All rights reserved.