请使用 x86 32位 ArchLinux GCC 4.5.0 环境编译文中 C++ 示例代码, 不过只要是 GCC 或 clang++ 编译结果应该都相仿.
作为一个特别的语言, C++ 中的左值是*语义*概念而非*语法*概念, 这一点甚至也可以认为是沿袭自 C 语言. 在任何其他语言中讨论 “什么是左值” 这个概念时, 会简短使用 “变量可以是左值”, “表达式不能是左值”, “常数不能作为左值” 等等, 诸如 “变量”, “表达式”, “常数” 这些都是语法概念, 所以刚才这些说法都是左值的语法约定. 确实一些语言使用语法约束来规定左值, 比如 Python, 下面的 Python 代码
class A:
pass
a = A()
a + 1 = 0
编译时会报 “SyntaxError: can’t assign to operator”, 是 *Syntax* 哦. -甚至连- JS -这样乱的语言-也有类似的例子, 比如
var f = function() {};
f() = 0;
在执行的时候会报 "ReferenceError: Invalid left-hand side in assignment" --- Chromium.
在 C 里面表达式, 或者函数的返回值, 或者两者的混合, 只要语法恰当, 返回值类型又是一个可写指针, 那么通过对该指针引用的方式来赋值, 如
int* f() { return NULL; }
*(f() + 1) = 0;
这一特性在 C++ 那放荡不羁的类型系统里得到了极大的加强, 完全不再受制于语法的约束, 编译器只需要先根据运算符优先级搭起语法树, 至于左值判定什么的都留在以后的语义分析再来.
而这一切的开端仅仅是因为 C++ 支持一个指针的替代, 也就是引用, 这样连前缀星号都省略了.
另一方面, C++ 中的运算符重载机制又极大地扩展其左值概念的超凡脱俗, 并且 C++ 程序员似乎从来也未有过 “哦, 这只是个示例程序, 尽量别引入复杂特性” 这样的想法, 即使是经典的 Hello World 程序中, 也毫不吝惜地显摆着运算符重载这种高级特性.
#include <iostream>
int main()
{
std::cout << "Hello, world\n";
}
从这个例子扩展出一个赋值语句几乎毫不费力, 只需要在函数体第一行加个 =
之后随手发挥就好.
#include <iostream>
int main()
{
std::cout << "Hello World\n" = std::cerr;
return 0;
}
当然这份代码会报错但不是因为左值不合理, 而是 --- 赋值操作符被设为私有函数或赋值操作符重载被删除了 (C++11 中).
如果希望的话, 可以进一步参考下面这个操作符重载错乱的例子 (它可能由一位危险的 C++ 实习生在主键盘区 +=
坏掉的机器上编写的)
struct type {
int x;
type& operator+(type& rhs)
{
this.x = rhs.x;
return *this;
}
type& operator=(type& rhs)
{
this.x += rhs.x;
return *this;
}
};
int main()
{
type a, b, c;
a + b = c;
return 0;
}
所以在 C++ 的世界里, 左值跟赋值运算操作符已经没有半点关系了, 倒是只要类型能够匹配上, 运算符重载就能通过编译, 看起来再离奇的算式都没问题.
不过, 如果非要扯赋值运算符, 它跟其它二元运算符有什么不同?
这差异还确实有, 那就是, 赋值运算符必须是成员非静态的, 也就是说下面的编译无法通过
struct type {};
type const& operator=(type&, type const&);
而其它二元运算符都允许以全局静态形式重载, 比如非常必需的流输入输出重载.
最后的最后, 这个规定合理吗? 这个问题就很难回答了, 目前我个人觉得这个限制是很无理的... 而且, 顺带说下, 下面的代码却是可以通过编译的
struct type {};
type const& operator+=(type&, type const&);
type const& operator&=(type&, type const&);