为什么复制赋值运算符必须返回引用/常量引用?

发布于 2024-09-07 12:07:26 字数 387 浏览 6 评论 0原文

在 C++ 中,我不清楚从复制赋值运算符返回引用的概念。为什么复制赋值运算符不能返回新对象的副本?另外,如果我有类 A 和以下内容:

A a1(param);
A a2 = a1;
A a3;

a3 = a2; //<--- this is the problematic line

operator= 定义如下:

A A::operator=(const A& a)
{
    if (this == &a)
    {
        return *this;
    }
    param = a.param;
    return *this;
}

In C++, the concept of returning reference from the copy assignment operator is unclear to me. Why can't the copy assignment operator return a copy of the new object? In addition, if I have class A, and the following:

A a1(param);
A a2 = a1;
A a3;

a3 = a2; //<--- this is the problematic line

The operator= is defined as follows:

A A::operator=(const A& a)
{
    if (this == &a)
    {
        return *this;
    }
    param = a.param;
    return *this;
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

柠檬色的秋千 2024-09-14 12:07:26

稍微澄清一下为什么最好通过 operator= 的引用返回而不是按值返回 --- 因为链 a = b = c 如果 a返回值。

如果您返回参考,则只需完成最少的工作。一个对象的值被复制到另一个对象。

但是,如果您按 operator= 的值返回,则每次调用赋值运算符时都会调用构造函数和析构函数!

所以,给定:

A& operator=(const A& rhs) { /* ... */ };

那么,

a = b = c; // calls assignment operator above twice. Nice and simple.

但是,

A operator=(const A& rhs) { /* ... */ };

a = b = c; // calls assignment operator twice, calls copy constructor twice, calls destructor type to delete the temporary values! Very wasteful and nothing gained!

总而言之,按价值返回没有任何收获,反而会失去很多。

注意:这并不是为了解决让赋值运算符返回左值的优点。请阅读其他文章以了解为什么这样做可能更好)

A bit of clarification as to why it's preferable to return by reference for operator= versus return by value --- as the chain a = b = c will work fine if a value is returned.

If you return a reference, minimal work is done. The values from one object are copied to another object.

However, if you return by value for operator=, you will call a constructor AND destructor EACH time that the assignment operator is called!!

So, given:

A& operator=(const A& rhs) { /* ... */ };

Then,

a = b = c; // calls assignment operator above twice. Nice and simple.

But,

A operator=(const A& rhs) { /* ... */ };

a = b = c; // calls assignment operator twice, calls copy constructor twice, calls destructor type to delete the temporary values! Very wasteful and nothing gained!

In sum, there is nothing gained by returning by value, but a lot to lose.

(Note: This isn't meant to address the advantages of having the assignment operator return an lvalue. Read the other posts for why that might be preferable)

执笏见 2024-09-14 12:07:26

严格来说,复制赋值运算符的结果不需要返回引用,尽管为了模仿 C++ 编译器使用的默认行为,它应该返回对被赋值对象的非常量引用(隐式生成的复制)赋值运算符将返回非常量引用 - C++03: 12.8/10)。我见过很多从复制赋值重载中返回 void 的代码,但我不记得这是什么时候导致了严重的问题。返回 void 将阻止用户“赋值链”(a = b = c;),并阻止在测试表达式中使用赋值结果。虽然这种代码绝不是闻所未闻,但我也不认为它特别常见 - 特别是对于非原始类型(除非类的接口旨在用于此类测试,例如 iostream)。

我并不是建议您这样做,只是指出这是允许的,而且它似乎不会引起很多问题。

这些其他 SO 问题是相关的(可能不完全是骗人的),其中包含您可能感兴趣的信息/意见。

Strictly speaking, the result of a copy assignment operator doesn't need to return a reference, though to mimic the default behavior the C++ compiler uses, it should return a non-const reference to the object that is assigned to (an implicitly generated copy assignment operator will return a non-const reference - C++03: 12.8/10). I've seen a fair bit of code that returns void from copy assignment overloads, and I can't recall when that caused a serious problem. Returning void will prevent users from 'assignment chaining' (a = b = c;), and will prevent using the result of an assignment in a test expression, for example. While that kind of code is by no means unheard of, I also don't think it's particularly common - especially for non-primitive types (unless the interface for a class intends for these kinds of tests, such as for iostreams).

I'm not recommending that you do this, just pointing out that it's permitted and that it doesn't seem to cause a whole lot of problems.

These other SO questions are related (probably not quite dupes) that have information/opinions that might be of interest to you.

倾城月光淡如水﹏ 2024-09-14 12:07:26

当您重载operator=时,您可以编写它来返回您想要的任何类型。如果您非常想要,可以重载 X::operator= 以返回(例如)某个完全不同的类 YZ 的实例代码>.不过,这通常是非常不可取的。

特别是,您通常希望像 C 一样支持 operator= 的链接。例如:

int x, y, z;

x = y = z = 0;

既然如此,您通常希望返回被分配类型的左值或右值。只剩下是否返回对 X 的引用、对 X 的 const 引用或 X(按值)的问题。

返回 X 的常量引用通常是一个糟糕的主意。特别是,允许​​ const 引用绑定到临时对象。临时变量的生命周期会延长到它所绑定的引用的生命周期,但不会递归地延长到可能分配给的任何内容的生命周期。这使得返回悬空引用变得很容易——const 引用绑定到一个临时对象。该对象的生命周期延长到引用的生命周期(在函数结束时结束)。当函数返回时,引用和临时的生命周期已经结束,因此分配的是悬空引用。

当然,返回非常量引用并不能提供完全的保护,但至少可以让您更加努力地工作。您仍然可以(例如)定义一些本地变量,并返回对它的引用(但大多数编译器也可以并且会对此发出警告)。

返回值而不是引用存在理论和实际问题。从理论上讲, = 通常的含义与在这种情况下的含义之间存在基本脱节。特别是,赋值通常意味着“获取此现有源并将其值分配给此现有目标”,它开始意味着更像“获取此现有源,创建它的副本,并将该值分配给此现有目标。 ”

从实际角度来看,尤其是在发明右值引用之前,这可能会对性能产生重大影响——在将 A 复制到 B 的过程中创建一个全新的对象是意想不到的,而且通常非常慢。例如,如果我有一个小向量,并将其分配给一个较大的向量,我预计最多需要时间来复制小向量的元素加上(一点)固定开销来调整大小目标向量。如果涉及两个个副本,一个从源到临时,另一个从临时到目标,以及(更糟糕的是)临时向量的动态分配,我对操作复杂性的期望将是完全被摧毁。对于小向量,动态分配的时间很容易比复制元素的时间高很多倍。

唯一的其他选项(在 C++11 中添加)是返回右值引用。这很容易导致意外结果 - 像 a=b=c; 这样的链式赋值可能会破坏 b 和/或 c 的内容,这将是非常出乎意料的。

这就使得返回普通引用(不是对 const 的引用,也不是右值引用)成为(合理地)可靠地产生大多数人通常想要的结果的唯一选项。

When you overload operator=, you can write it to return whatever type you want. If you want to badly enough, you can overload X::operator= to return (for example) an instance of some completely different class Y or Z. This is generally highly inadvisable though.

In particular, you usually want to support chaining of operator= just like C does. For example:

int x, y, z;

x = y = z = 0;

That being the case, you usually want to return an lvalue or rvalue of the type being assigned to. That only leaves the question of whether to return a reference to X, a const reference to X, or an X (by value).

Returning a const reference to X is generally a poor idea. In particular, a const reference is allowed to bind to a temporary object. The lifetime of the temporary is extended to the lifetime of the reference to which it's bound--but not recursively to the lifetime of whatever that might be assigned to. This makes it easy to return a dangling reference--the const reference binds to a temporary object. That object's lifetime is extended to the lifetime of the reference (which ends at the end of the function). By the time the function returns, the lifetime of the reference and temporary have ended, so what's assigned is a dangling reference.

Of course, returning a non-const reference doesn't provide complete protection against this, but at least makes you work a little harder at it. You can still (for example) define some local, and return a reference to it (but most compilers can and will warn about this too).

Returning a value instead of a reference has both theoretical and practical problems. On the theoretical side, you have a basic disconnect between = normally means and what it means in this case. In particular, where assignment normally means "take this existing source and assign its value to this existing destination", it starts to mean something more like "take this existing source, create a copy of it, and assign that value to this existing destination."

From a practical viewpoint, especially before rvalue references were invented, that could have a significant impact on performance--creating an entire new object in the course of copying A to B was unexpected and often quite slow. If, for example, I had a small vector, and assigned it to a larger vector, I'd expect that to take, at most, time to copy elements of the small vector plus a (little) fixed overhead to adjust the size of the destination vector. If that instead involved two copies, one from source to temp, another from temp to destination, and (worse) a dynamic allocation for the temporary vector, my expectation about the complexity of the operation would be entirely destroyed. For a small vector, the time for the dynamic allocation could easily be many times higher than the time to copy the elements.

The only other option (added in C++11) would be to return an rvalue reference. This could easily lead to unexpected results--a chained assignment like a=b=c; could destroy the contents of b and/or c, which would be quite unexpected.

That leaves returning a normal reference (not a reference to const, nor an rvalue reference) as the only option that (reasonably) dependably produces what most people normally want.

最舍不得你 2024-09-14 12:07:26

部分原因是返回对 self 的引用比按值返回更快,但除此之外,它还允许原始类型中存在的原始语义。

It's partly because returning a reference to self is faster than returning by value, but in addition, it's to allow the original semantics that exist in primitive types.

雾里花 2024-09-14 12:07:26

operator= 可以定义为返回您想要的任何内容。您需要更具体地了解问题的实际情况;我怀疑您的复制构造函数在内部使用 operator= ,这会导致堆栈溢出,因为复制构造函数调用 operator= ,它必须使用复制构造函数返回 A 的价值无限。

operator= can be defined to return whatever you want. You need to be more specific as to what the problem actually is; I suspect that you have the copy constructor use operator= internally and that causes a stack overflow, as the copy constructor calls operator= which must use the copy constructor to return A by value ad infinitum.

递刀给你 2024-09-14 12:07:26

对于用户定义的 operator= 的结果类型没有核心语言要求,但标准库确实有这样的要求:

C++98 §23.1/3:

这些组件中存储的对象类型必须满足CopyConstructible的要求
类型 (20.1.3),以及 Assignable 类型的附加要求。

C++98 §23.1/4:

在表 64 中,T 是用于实例化容器的类型,tTT,而 u 是一个值(可能是 constT

在此处输入图像描述


按值返回副本仍将支持赋值链,如 a = b = c = 42;,因为赋值运算符是右结合的,即被解析为a = (b = (c = 42));。但返回副本会禁止像 (a = b) = 666; 这样无意义的构造。对于较小的类,返回副本可能是最有效的,而对于较大的类,通过引用返回通常是最有效的(而副本则效率极低)。

在了解标准库要求之前,我曾经让 operator= 返回 void,以提高效率并避免支持基于副作用的不良代码的荒谬。


对于 C++11,对于赋值运算符的 default 还需要 T& 结果类型,因为

C++11 §8.4.2/1:

显式默认的函数应 [...] 具有相同的声明函数类型(除了可能不同的引用限定符以及除了
对于复制构造函数或复制赋值运算符,参数类型可以是“对非 const T 的引用”,其中 T 是成员函数的类的名称)就好像它已经被隐式声明了

There is no core language requirement on the result type of a user-defined operator=, but the standard library does have such a requirement:

C++98 §23.1/3:

The type of objects stored in these components must meet the requirements of CopyConstructible
types (20.1.3), and the additional requirements of Assignable types.

C++98 §23.1/4:

In Table 64, T is the type used to instantiate the container, t is a value of T, and u is a value of (possibly const) T.

enter image description here


Returning a copy by value would still support assignment chaining like a = b = c = 42;, because the assignment operator is right-associative, i.e. this is parsed as a = (b = (c = 42));. But returning a copy would prohibit meaningless constructions like (a = b) = 666;. For a small class returning a copy could conceivably be most efficient, while for a larger class returning by reference will generally be most efficient (and a copy, prohibitively inefficient).

Until I learned about the standard library requirement I used to let operator= return void, for efficiency and to avoid the absurdity of supporting side-effect based bad code.


With C++11 there is additionally the requirement of T& result type for default-ing the assignment operator, because

C++11 §8.4.2/1:

A function that is explicitly defaulted shall […] have the same declared function type (except for possibly differing ref-qualifiers and except that in
the case of a copy constructor or copy assignment operator, the parameter type may be “reference to non-const T”, where T is the name of the member function’s class) as if it had been implicitly declared

凡尘雨 2024-09-14 12:07:26
  1. 复制赋值不应为 void,否则赋值链将无法工作
a = b = c; 
// because assignment operator is right-associative
// it is equal to
a = (b = c);  // oops, (b = c) return nothing, the code won't compile
  1. 复制赋值不应返回值,否则将调用不必要的复制构造函数和析构函数
// suppose a, b and c are of type X, which holds some resource that will take efforts to copy
a = b = c;
// is equal to
X temp1.X::( (b = c) ); // copy constructor called once
X temp2.X::( a.X::operator=(temp1) ); // copy constructor called twice; temp1 destructed inside a.X::operator=(temp1)
  1. 复制赋值不应返回右值引用,因为它可能具有已赋值的值对象移动。再次以赋值链为例
a = b = c;
// if a has a copy assignment overload that takes rvalue reference as argument like the following
X& operator=(X &&);
// then the result of (b = c) will be moved into a, and make b an invalid object afterwards
  1. Copy assignment should not be void, otherwise assignment chain will not work
a = b = c; 
// because assignment operator is right-associative
// it is equal to
a = (b = c);  // oops, (b = c) return nothing, the code won't compile
  1. Copy assignment should not return a value, otherwise unnecessary copy constructor and destructor will be called
// suppose a, b and c are of type X, which holds some resource that will take efforts to copy
a = b = c;
// is equal to
X temp1.X::( (b = c) ); // copy constructor called once
X temp2.X::( a.X::operator=(temp1) ); // copy constructor called twice; temp1 destructed inside a.X::operator=(temp1)
  1. Copy assignment should not return rvalue reference cos it may have the assigned object moved. Again take the assignment chain for example
a = b = c;
// if a has a copy assignment overload that takes rvalue reference as argument like the following
X& operator=(X &&);
// then the result of (b = c) will be moved into a, and make b an invalid object afterwards
十二 2024-09-14 12:07:26

我想,因为用户定义的对象应该表现得像内置类型。
例如:

char c;
while ((c = getchar()) != -1 ) {/* do the stuff */}

I guess, because user defined object should behave like builtin types.
For example:

char c;
while ((c = getchar()) != -1 ) {/* do the stuff */}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文