5.4. 黑暗法则

在C++语言中,有一个鲜为人知却令人颇感意外的“黑暗角落” —— 当某些源自这一特性的代码偶然出现时,会让开发者们陷入困惑,甚至引发大规模的重构。一个典型的例子就是数组访问的写法:为什么在C++中,a[2]与2[a]是完全等效的?

int main() {
  int a[16] = {0};
  a[2] = 3;
  3[a] = 4;
}

这段看似怪异的代码能够编译,其根本原因在于:指针的算术本质。

  • a[i]可转化为*(a + i)
  • i[a]同样可转化为*(i + a)
  • 加法交换律使这两种形式完全等价

现代容器的限制。std::vector和std::array不支持这种"乱序"语法:

  • 运算符重载规则:operator[]作为成员函数必须通过对象实例调用
  • 安全设计理念:内部实现采用边界检查而非原始指针算术

模拟方案(不建议生产环境使用):

#include <vector>
#include <iostream>
struct wrapper {
  wrapper(int p) : i(p) {}
  int operator[](const std::vector<int> v) {return v[i];}
  int i = 0;
};
struct helper {
  helper() = default;
  wrapper operator << (int a) { return wrapper {a}; }
};
#define _ helper()<<
int main() {
  std::vector<int> vec = {10, 20, 30, 40, 50};
  int b= (_ 2) [vec];
  std::cout << b << std::endl; // Outputs 30
  return 0;
}

经过快速审视这段代码后(声明:两位作者中Alex是无辜的,此处使用"我们"仅是行文惯例),必须承认 —— 这段代码实在难登大雅之堂,更不敢将其实现到std::array或其他容器中。

但仔细看来,其中仍存在有趣的技术点。虽然最初目标是实现vector/array的无序索引访问,但残酷的现实是:这根本不可行。尝试编译2[vec]表达式时,编译器会直接报错:

error: no match for 'operator[]' (operand types are 'int' and 'std::vector<int>')

这个错误信息直白地说就是:编译器找不到一个能接受整型参数并应用于int向量的下标运算符。在C++现有的语法体系下,这种写法永远不可能通过编译,主要原因有二:

1. 成员函数限制:

  • operator[]必须定义为类的成员函数
  • C++不允许存在独立的下标运算符(不存在全局的operator[])

2. 运算符优先级机制:

  • 在C++中,操作符优先级(operator precedence)决定了表达式的解析顺序
  • 高优先级运算符先于低优先级运算符求值
  • 当优先级相同时,由结合性(associativity)决定求值顺序

虽然最新标准第7章[expr.pre]节指出"运算符优先级并非直接规定,而是通过语法推导",但仍有权威资料https://en.cppreference.com/w/cpp/language/operator_precedence完整列出了优先级顺序表 —— 强烈建议开发者系统阅读这些资料。

5.4.1 最关键的问题

此刻各位读者应该能轻松回答这个问题了:下面这段程序的输出结果是什么?

#include <iostream>
int main() {
  auto a = 4;
  std::cout << sizeof(a)["Hello World"] << std::endl;
  return 0;
}

在将代码扔进编译器之前,不妨暂停片刻,沉下心来仔细推敲其中的运行机制。本节已为提供了所有解题线索 —— 从关键提示到潜在方向一应俱全。我们暂不揭晓答案,也不做完整解析,仅列出现象要点,相信这些足以帮助您得出正确结论:

  • 在表达式 auto a = 4; 中,变量 a 推导为 int 类型并初始化为 4 —— 这正是现代 C++ 中 auto 与整数字面量的配合方式。
  • 接下来是精妙之处:通过代码解析可知,sizeof(a) 表达式将返回 sizeof(int) 的值:
    • 主流系统中通常为 4 字节
    • 古老的 16 位系统可能为 2 字节
    • 某些特殊架构可能达到 8 字节(虽然笔者从未亲眼见过)

这正是推理的关键转折点 —— C++的运算符优先级在此发挥了决定性作用。以下是从优先级表中提取的与当前案例直接相关的部分:

优先级 操作符 描述
1 :: 命名空间解析操作符
2 a++
a--
后缀自增和自减
a() 函数调用
a[] 下标
3 ++a
--a
前缀自增和自减
+a
-a
一元正负
! ~ 逻辑非和位非
*a 解引用
&a 取地址
sizeof sizeof(内存大小)运算符

现在我们可以更清晰地理解:在这段代码中,sizeof(a)实际上并不会被真正执行。这是由于C++编译器在处理运算符优先级时的规则 —— [] 运算符的优先级高于 sizeof,因此编译器会优先解析 (a)["Hello World"] 这个表达式。

关键解析步骤:

括号的等价性:在C++中,(a)基本等同于a(除非遇到"最令人头疼的解析"场景,这个我们稍后再讨论)。因此整个表达式等价于sizeof a["Hello World"]

数组访问的对称性:如前所述,a["Hello World"]"Hello World"[a]效果相同。当a的值为4时,这将返回字符串中的字符‘o’。

sizeof运算符的特性:表达式最终简化为sizeof 'o',对于char类型,sizeof运算结果总是1。

至此,问题的答案应该已经显而易见了。