提问人:bks 提问时间:6/24/2010 最后编辑:Azeembks 更新时间:11/2/2022 访问量:56363
为什么复制赋值运算符必须返回 reference/const 引用?[复制]
Why must the copy assignment operator return a reference/const reference? [duplicate]
问:
在 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;
}
答:
这在一定程度上是因为返回对 self 的引用比按值返回更快,但除此之外,它还允许原始类型中存在的原始语义。
评论
operator=
可以定义为返回您想要的任何内容。您需要更具体地说明问题到底是什么;我怀疑您让复制构造函数在内部使用,这会导致堆栈溢出,因为复制构造函数调用必须使用复制构造函数无限期返回值。operator=
operator=
A
评论
A&
A::operator=
严格来说,复制赋值运算符的结果不需要返回引用,但要模拟 C++ 编译器使用的默认行为,它应该返回对分配给的对象的非常量引用(隐式生成的复制赋值运算符将返回非恒量引用 - C++03:12.8/10)。我见过相当多的代码从复制分配重载返回,我不记得这是什么时候导致的严重问题。例如,返回将阻止用户进行“赋值链接”(),并且将阻止在测试表达式中使用赋值结果。虽然这种代码绝不是闻所未闻的,但我也不认为它特别常见——尤其是对于非原始类型(除非类的接口用于这些类型的测试,例如 iostreams)。void
void
a = b = c;
我不建议你这样做,只是指出这是允许的,而且它似乎不会引起很多问题。
这些其他 SO 问题是相关的(可能不是完全重复的),其中包含您可能感兴趣的信息/意见。
评论
稍微澄清一下为什么最好按引用返回而不是按值返回---因为如果返回一个值,链将正常工作。operator=
a = b = c
如果返回引用,则完成的工作最少。将一个对象中的值复制到另一个对象。
但是,如果按值返回 ,则每次调用赋值运算符时,都会调用构造函数和析构函数!!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!
总而言之,按价值回报没有任何好处,但会损失很多。
(注意:这并不是为了解决赋值运算符返回左值的优势。阅读其他帖子,了解为什么这可能更可取)
评论
当你重载时,你可以写它来返回你想要的任何类型。如果你想要足够糟糕,你可以重载以返回(例如)某个完全不同的类或 .不过,这通常是非常不可取的。operator=
X::operator=
Y
Z
特别是,您通常希望像 C 一样支持链接。例如:operator=
int x, y, z;
x = y = z = 0;
在这种情况下,您通常希望返回所分配类型的左值或右值。这只剩下一个问题,即是返回对 X 的引用、对 X 的常量引用还是 X(按值)。
返回对 X 的常量引用通常是一个糟糕的主意。具体而言,允许常量引用绑定到临时对象。临时值的生存期将延长到它所绑定到的引用的生存期,但不会递归地扩展到可能分配给的任何引用的生存期。这样可以很容易地返回一个悬空的引用 -- const 引用绑定到一个临时对象。该对象的生存期将延长到引用的生存期(在函数结束时结束)。当函数返回时,引用和临时引用的生存期已经结束,因此分配的是一个悬空的引用。
当然,返回一个非常量引用并不能完全防止这种情况,但至少会让你更加努力地工作。您仍然可以(例如)定义一些本地,并返回对它的引用(但大多数编译器也可以并且会对此发出警告)。
返回值而不是引用既有理论问题,也有实践问题。从理论上讲,通常的均值和本例中的含义之间存在基本的脱节。特别是,赋值通常意味着“获取此现有源并将其值分配给此现有目标”,它开始意味着更像是“获取此现有源,创建它的副本,并将该值分配给此现有目标”。=
从实际的角度来看,尤其是在右值引用被发明之前,这可能会对性能产生重大影响——在将 A 复制到 B 的过程中创建一个全新的对象是出乎意料的,而且通常非常缓慢。例如,如果我有一个小向量,并将其分配给一个较大的向量,我预计最多需要时间来复制小向量的元素加上(小)固定开销来调整目标向量的大小。如果这涉及两个副本,一个从源到临时,另一个从临时到目标,以及(更糟糕的是)临时向量的动态分配,我对操作复杂性的期望将被完全破坏。对于小向量,动态分配的时间很容易比复制元素的时间高很多倍。
唯一的其他选项(在 C++11 中添加)是返回右值引用。这很容易导致意想不到的结果 - 像这样的链式赋值可能会破坏 and/or 的内容,这将是非常出乎意料的。a=b=c;
b
c
这样一来,返回一个普通的引用(不是对 const 的引用,也不是右值引用)是唯一的选择,它(合理地)可靠地产生大多数人通常想要的东西。
评论
const T &ref = T{} = t;
operator=
T&
T const &
operator=
lvalue reference
对用户定义的结果类型没有核心语言要求,但标准库确实有这样的要求:operator=
C++98 §23.1/3:
“这些组件中存储的对象类型必须满足类型 (20.1.3) 的要求,以及类型的附加要求。
CopyConstructible
Assignable
C++98 §23.1/4:
“在表 64 中,是用于实例化容器的类型,是 的值 ,并且是 的值(可能)。
T
t
T
u
const
T
按值返回副本仍将支持赋值链接,例如 ,因为赋值运算符是右关联的,即这被解析为 。但是返回副本会禁止像 .对于一个小类来说,返回一个副本可能是最有效的,而对于一个较大的类来说,通过引用返回通常是最有效的(而一个副本,效率低得令人望而却步)。a = b = c = 42;
a = (b = (c = 42));
(a = b) = 666;
在我了解了标准库要求之前,我曾经让返回,以提高效率并避免支持基于副作用的不良代码的荒谬性。operator=
void
使用 C++11 时,赋值运算符 -ing 还需要结果类型,因为T&
default
C++11 §8.4.2/1:
“显式默认的函数应具有相同的声明函数类型(除了可能不同的 ref-qualifiers 和 在复制构造函数或复制赋值运算符的情况下,参数类型可以是“对非常量的引用”,其中是成员函数类的名称),就好像它已被隐式声明一样
T
T
我猜,因为用户定义的对象应该像内置类型一样运行。 例如:
char c;
while ((c = getchar()) != -1 ) {/* do the stuff */}
- 复制分配不应是 ,否则分配链将不起作用
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
- 复制赋值不应返回值,否则将调用不必要的复制构造函数和析构函数
// 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)
- 复制赋值不应返回右值引用,因为它可能移动了赋值对象。再次以分配链为例
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
评论
A&
a=b
a
a
b
A*
A*
A & operator=(A a);