TL;DR
额外的括号会改变 C++ 程序在以下上下文中的含义:
- 阻止依赖于参数的名称查找
- 在列表上下文中启用逗号运算符
- 令人烦恼的解析的歧义解决
- 推断
decltype 表达式中的引用
- 防止预处理器宏错误
防止参数依赖的名称查找
如标准附录 A 中所述,(expression) 形式的 post-fix expression 是 primary expression,但不是 id-expression,因此不是 unqualified-id。这意味着与传统形式 fun(arg) 相比,在形式为 (fun)(arg) 的函数调用中阻止了依赖于参数的名称查找。
3.4.2 依赖于参数的名称查找 [basic.lookup.argdep]
1 当函数调用(5.2.2)中的后缀表达式是
unqualified-id,平时不考虑的其他命名空间
可以搜索不合格的查找(3.4.1),并且在这些命名空间中,
命名空间范围的友元函数或函数模板声明
(11.3) 可能会发现其他不可见的。这些修改对
搜索取决于参数的类型(以及模板模板
arguments,模板参数的命名空间)。 [ 例子:
namespace N {
struct S { };
void f(S);
}
void g() {
N::S s;
f(s); // OK: calls N::f
(f)(s); // error: N::f not considered; parentheses
// prevent argument-dependent lookup
}
——结束示例]
在列表上下文中启用逗号运算符
逗号运算符在大多数类似列表的上下文(函数和模板参数、初始化列表等)中具有特殊含义。在这种情况下,a, (b, c), d 形式的括号可以启用逗号运算符,而常规形式 a, b, c, d 不适用逗号运算符。
5.18 逗号运算符 [expr.comma]
2 在逗号被赋予特殊含义的上下文中,[ 示例:in
函数的参数列表(5.2.2)和初始化器列表
(8.5) —end example ] 第 5 条中描述的逗号操作符可以
仅出现在括号中。 [ 例子:
f(a, (t=3, t+2), c);
有三个参数,其中第二个参数的值为 5。——结束示例
]
烦恼解析的歧义解决
向后兼容 C 及其神秘的函数声明语法可能会导致令人惊讶的解析歧义,即令人烦恼的解析。本质上,任何可以被解析为声明的东西都将被解析为一个,即使竞争解析也适用。
6.8 歧义分辨率 [stmt.ambig]
1 涉及表达式语句的语法存在歧义
和声明:具有函数样式的表达式语句
显式类型转换(5.2.3)作为其最左边的子表达式可以是
与第一个声明符开始的声明没有区别
带有 (。在这些情况下,该语句是一个声明。
8.2 歧义分辨率 [dcl.ambig.res]
1 由函数样式之间的相似性引起的歧义
cast 和 6.8 中提到的声明也可以出现在上下文中
的声明。在这种情况下,选择是在一个函数之间
在参数周围带有一组冗余括号的声明
名称和具有函数样式转换的对象声明作为
初始化器。正如 6.8 中提到的歧义一样,
解决方案是考虑任何可能是
声明声明。 [注意:声明可以显式
通过非函数式强制转换消除歧义,通过 = 表示
初始化或删除多余的括号
参数名称。 ——尾注] [示例:
struct S {
S(int);
};
void foo(double a) {
S w(int(a)); // function declaration
S x(int()); // function declaration
S y((int)a); // object declaration
S z = int(a); // object declaration
}
——结束示例]
一个著名的例子是Most Vexing Parse,这个名字由 Scott Meyers 在他的Effective STL 书的第 6 项中推广:
ifstream dataFile("ints.dat");
list<int> data(istream_iterator<int>(dataFile), // warning! this doesn't do
istream_iterator<int>()); // what you think it does
这声明了一个函数data,其返回类型为list<int>。这
函数数据有两个参数:
- 第一个参数名为
dataFile。它的类型是istream_iterator<int>。这
dataFile 周围的括号是多余的,将被忽略。
- 第二个参数没有名字。它的类型是指向函数的指针
什么都没有,返回一个
istream_iterator<int>。
在第一个函数参数周围放置额外的括号(第二个参数周围的括号是非法的)将解决歧义
list<int> data((istream_iterator<int>(dataFile)), // note new parens
istream_iterator<int>()); // around first argument
// to list's constructor
C++11 具有大括号初始化器语法,允许在许多上下文中回避此类解析问题。
推断decltype 表达式中的引用
与auto 类型推导相比,decltype 允许推导引用(左值和右值引用)。规则区分decltype(e) 和decltype((e)) 表达式:
7.1.6.2 简单类型说明符 [dcl.type.simple]
4 对于表达式e,decltype(e)表示的类型定义为
如下:
——如果e 是一个未加括号的 id 表达式或
无括号类成员访问(5.2.5),decltype(e) 是类型
由e 命名的实体。如果没有这样的实体,或者如果e 命名为
一组重载函数,程序格式错误;
——否则,
如果e 是一个xvalue,decltype(e) 是T&&,其中T 是e 的类型;
——
否则,如果e 是左值,则decltype(e) 是T&,其中T 是类型
e;
— 否则,decltype(e) 是 e 的类型。
的操作数
decltype 说明符是一个未计算的操作数(第 5 条)。 [ 例子:
const int&& foo();
int i;
struct A { double x; };
const A* a = new A();
decltype(foo()) x1 = 0; // type is const int&&
decltype(i) x2; // type is int
decltype(a->x) x3; // type is double
decltype((a->x)) x4 = x3; // type is const double&
—结束示例] [注:确定类型的规则涉及
decltype(auto) 在 7.1.6.4 中指定。 ——尾注]
decltype(auto) 的规则对于初始化表达式的 RHS 中的额外括号具有类似的含义。这是来自C++FAQ 和this related Q&A 的示例
decltype(auto) look_up_a_string_1() { auto str = lookup1(); return str; } //A
decltype(auto) look_up_a_string_2() { auto str = lookup1(); return(str); } //B
第一个返回string,第二个返回string &,这是对局部变量str的引用。
防止预处理器宏相关的错误
预处理器宏在与 C++ 语言本身的交互中存在许多微妙之处,其中最常见的如下所列
- 在宏定义
#define TIMES(A, B) (A) * (B); 内使用括号括住宏参数以避免不需要的运算符优先级(例如,在TIMES(1 + 2, 2 + 1) 中,它产生9,但如果没有(A) 和(B) 周围的括号,将产生6
- 在包含逗号的宏参数周围使用括号:
assert((std::is_same<int, int>::value));,否则将无法编译
- 在函数周围使用括号来防止包含的标头中的宏扩展:
(min)(a, b)(还有禁用 ADL 的不良副作用)