为什么内置赋值 return 是 non-const 引用而不是 C++ 中的 const 引用?
why does builtin assignment return a non-const reference instead of a const reference in C++?
(注意原来的问题标题有 "instead of an rvalue" 而不是 "instead of a const reference"。下面的答案之一是对旧标题的回应。为清楚起见已修复)
C 和 C++ 中的一个常见结构是链式赋值,例如
int j, k;
j = k = 1;
第二个=
先执行,表达式k=1
的副作用是k
设置为1,而表达式本身的值为1。
但是,以下是一种在 C++ 中合法(但在 C 中不合法)的构造,它对所有基类型都有效:
int j, k=2;
(j=k) = 1;
这里,表达式j=k
的副作用是将j
设置为2,表达式本身成为对j
的引用,然后设置j
到 1. 据我了解,这是因为表达式 j=k
return 是 non-const
int&
,例如一般来说是左值。
此约定通常也推荐用于 user-defined 类型,如 Meyers Effective C++ 中的 "Item 10: Have assignment operators return a (non-const) reference to *this" 中所述(我的括号加法)。本书的那一部分并没有试图解释为什么参考文献是非 const
的,甚至也没有顺便指出非 const
ness。
当然,这肯定增加了功能,但是 (j=k) = 1;
的语句至少可以说看起来很别扭。
如果约定改为使用内置赋值 return const 引用,那么自定义 类 也将使用此约定,并且原始链式构造在 C 中允许仍然有效,没有任何无关的副本或移动。例如,以下正确运行:
#include <iostream>
using std::cout;
struct X{
int k;
X(int k): k(k){}
const X& operator=(const X& x){
// the first const goes against convention
k = x.k;
return *this;
}
};
int main(){
X x(1), y(2), z(3);
x = y = z;
cout << x.k << '\n'; // prints 3
}
优点是所有 3 种(C 内置、C++ 内置和 C++ 自定义类型)在不允许像 (j=k) = 1
.
这样的习语方面都是一致的
在 C 和 C++ 之间添加这个习语是故意的吗?如果是这样,什么类型的情况可以证明它的使用是合理的?换句话说,non-spurious 这种功能扩展有什么好处?
我会回答标题中的问题。
让我们假设它return编辑了一个右值引用。不可能以这种方式 return 对新分配的 object 的引用(因为它是左值)。如果无法 return 引用新分配的 object,则需要创建一个副本。对于重型 objects,例如容器,这将是非常低效的。
考虑一个 class 类似于 std::vector 的例子。
对于当前的 return 类型,赋值是这样进行的(我没有使用模板和 copy-and-swap 惯用语,以使代码尽可能简单):
class vector {
vector& operator=(const vector& other) {
// Do some heavy internal copying here.
// No copy here: I just effectively return this.
return *this;
}
};
让我们假设它return编辑了一个右值:
class vector {
vector operator=(const vector& other) {
// Do some heavy stuff here to update this.
// A copy must happen here again.
return *this;
}
};
您可能会考虑 return 右值引用,但这也行不通:您不能只移动 *this
(否则,一连串的赋值 a = b = c
会 运行 b
),因此还需要第二份副本才能 return 它。
你的 post 的 body 中的问题是不同的: returning a const vector&
确实是可能的,没有上面显示的任何并发症,所以它看起来对我来说更像是一种约定。
注意:问题的标题指的是 built-ins,而我的回答涵盖了自定义的 classes。我相信这与一致性有关。如果它对 built-in 和自定义类型的行为不同,那将是非常令人惊讶的。
根据设计,C 和 C++ 之间的一个根本区别是 C 是一种 左值丢弃 语言,而 C++ 是一种 左值保留 语言。
在 C++98 之前,Bjarne 添加了对该语言的引用以使运算符重载成为可能。为了有用,引用需要保留而不是丢弃表达式的左值。
这种保留左值的想法直到 C++98 才真正形式化。在 C++98 标准之前的讨论中,引用要求保留表达式的左值这一事实得到了注意和形式化,这就是 C++ 与 C 做出重大且有目的的突破并成为一种左值保留语言的时候。
C++ 尽可能长时间地保留任何表达式结果的 "lvalueness"。它适用于所有内置运算符,也适用于内置赋值运算符。当然,还没有启用像 (a = b) = c
这样的表达式,因为它们的行为是未定义的(至少在原始 C++ 标准下)。但是因为 C++ 的这个 属性 你可以写出像
这样的代码
int a, b = 42;
int *p = &(a = b);
它有多大用处是另一个问题,但同样,这只是 lvalue-preserving C++ 表达式设计的结果之一。
至于为什么它不是 const
左值...坦率地说,我不明白为什么它应该是。与 C++ 中的任何其他左值保留内置运算符一样,它只保留给它的任何类型。
内置运算符 "return" 什么都没有,更不用说 "return a reference"。
表达式主要有两个特征:
- 他们的类型
- 他们的价值类别。
例如 k + 1
具有类型 int
和值类别 "prvalue",但 k = 1
具有类型 int
和值类别 "lvalue"。左值是指定内存位置的表达式,k = 1
指定的位置与声明 int k;
.
分配的位置相同
C 标准只有值类别 "lvalue" 和 "not lvalue"。在 C k = 1
中有类型 int
和类别 "not lvalue".
您似乎在暗示 k = 1
应该具有类型 const int
和值类别 lvalue
。也许可以,语言会略有不同。它会取缔令人困惑的代码,但也可能取缔有用的代码。对于语言设计者或设计委员会来说,这是一个很难评估的决定,因为他们无法想到该语言的所有可能使用方式。
他们犯了错误,没有引入可能会导致没有人预见到的问题的限制。一个相关的例子是 Should implicitly generated assignment operators be & ref-qualified?。
想到的一种可能情况是:
void foo(int& x);
int y;
foo(y = 3);
会将 y
设置为 3
,然后调用 foo
。根据您的建议,这是不可能的。当然你可以争辩说 y = 3; foo(y);
无论如何都更清楚,但这是一个滑坡:也许增量运算符不应该被允许在更大的表达式等中等等。
(注意原来的问题标题有 "instead of an rvalue" 而不是 "instead of a const reference"。下面的答案之一是对旧标题的回应。为清楚起见已修复)
C 和 C++ 中的一个常见结构是链式赋值,例如
int j, k;
j = k = 1;
第二个=
先执行,表达式k=1
的副作用是k
设置为1,而表达式本身的值为1。
但是,以下是一种在 C++ 中合法(但在 C 中不合法)的构造,它对所有基类型都有效:
int j, k=2;
(j=k) = 1;
这里,表达式j=k
的副作用是将j
设置为2,表达式本身成为对j
的引用,然后设置j
到 1. 据我了解,这是因为表达式 j=k
return 是 non-const
int&
,例如一般来说是左值。
此约定通常也推荐用于 user-defined 类型,如 Meyers Effective C++ 中的 "Item 10: Have assignment operators return a (non-const) reference to *this" 中所述(我的括号加法)。本书的那一部分并没有试图解释为什么参考文献是非 const
的,甚至也没有顺便指出非 const
ness。
当然,这肯定增加了功能,但是 (j=k) = 1;
的语句至少可以说看起来很别扭。
如果约定改为使用内置赋值 return const 引用,那么自定义 类 也将使用此约定,并且原始链式构造在 C 中允许仍然有效,没有任何无关的副本或移动。例如,以下正确运行:
#include <iostream>
using std::cout;
struct X{
int k;
X(int k): k(k){}
const X& operator=(const X& x){
// the first const goes against convention
k = x.k;
return *this;
}
};
int main(){
X x(1), y(2), z(3);
x = y = z;
cout << x.k << '\n'; // prints 3
}
优点是所有 3 种(C 内置、C++ 内置和 C++ 自定义类型)在不允许像 (j=k) = 1
.
在 C 和 C++ 之间添加这个习语是故意的吗?如果是这样,什么类型的情况可以证明它的使用是合理的?换句话说,non-spurious 这种功能扩展有什么好处?
我会回答标题中的问题。
让我们假设它return编辑了一个右值引用。不可能以这种方式 return 对新分配的 object 的引用(因为它是左值)。如果无法 return 引用新分配的 object,则需要创建一个副本。对于重型 objects,例如容器,这将是非常低效的。
考虑一个 class 类似于 std::vector 的例子。
对于当前的 return 类型,赋值是这样进行的(我没有使用模板和 copy-and-swap 惯用语,以使代码尽可能简单):
class vector {
vector& operator=(const vector& other) {
// Do some heavy internal copying here.
// No copy here: I just effectively return this.
return *this;
}
};
让我们假设它return编辑了一个右值:
class vector {
vector operator=(const vector& other) {
// Do some heavy stuff here to update this.
// A copy must happen here again.
return *this;
}
};
您可能会考虑 return 右值引用,但这也行不通:您不能只移动 *this
(否则,一连串的赋值 a = b = c
会 运行 b
),因此还需要第二份副本才能 return 它。
你的 post 的 body 中的问题是不同的: returning a const vector&
确实是可能的,没有上面显示的任何并发症,所以它看起来对我来说更像是一种约定。
注意:问题的标题指的是 built-ins,而我的回答涵盖了自定义的 classes。我相信这与一致性有关。如果它对 built-in 和自定义类型的行为不同,那将是非常令人惊讶的。
根据设计,C 和 C++ 之间的一个根本区别是 C 是一种 左值丢弃 语言,而 C++ 是一种 左值保留 语言。
在 C++98 之前,Bjarne 添加了对该语言的引用以使运算符重载成为可能。为了有用,引用需要保留而不是丢弃表达式的左值。
这种保留左值的想法直到 C++98 才真正形式化。在 C++98 标准之前的讨论中,引用要求保留表达式的左值这一事实得到了注意和形式化,这就是 C++ 与 C 做出重大且有目的的突破并成为一种左值保留语言的时候。
C++ 尽可能长时间地保留任何表达式结果的 "lvalueness"。它适用于所有内置运算符,也适用于内置赋值运算符。当然,还没有启用像 (a = b) = c
这样的表达式,因为它们的行为是未定义的(至少在原始 C++ 标准下)。但是因为 C++ 的这个 属性 你可以写出像
int a, b = 42;
int *p = &(a = b);
它有多大用处是另一个问题,但同样,这只是 lvalue-preserving C++ 表达式设计的结果之一。
至于为什么它不是 const
左值...坦率地说,我不明白为什么它应该是。与 C++ 中的任何其他左值保留内置运算符一样,它只保留给它的任何类型。
内置运算符 "return" 什么都没有,更不用说 "return a reference"。
表达式主要有两个特征:
- 他们的类型
- 他们的价值类别。
例如 k + 1
具有类型 int
和值类别 "prvalue",但 k = 1
具有类型 int
和值类别 "lvalue"。左值是指定内存位置的表达式,k = 1
指定的位置与声明 int k;
.
C 标准只有值类别 "lvalue" 和 "not lvalue"。在 C k = 1
中有类型 int
和类别 "not lvalue".
您似乎在暗示 k = 1
应该具有类型 const int
和值类别 lvalue
。也许可以,语言会略有不同。它会取缔令人困惑的代码,但也可能取缔有用的代码。对于语言设计者或设计委员会来说,这是一个很难评估的决定,因为他们无法想到该语言的所有可能使用方式。
他们犯了错误,没有引入可能会导致没有人预见到的问题的限制。一个相关的例子是 Should implicitly generated assignment operators be & ref-qualified?。
想到的一种可能情况是:
void foo(int& x);
int y;
foo(y = 3);
会将 y
设置为 3
,然后调用 foo
。根据您的建议,这是不可能的。当然你可以争辩说 y = 3; foo(y);
无论如何都更清楚,但这是一个滑坡:也许增量运算符不应该被允许在更大的表达式等中等等。