除了运算符优先级之外,额外的括号何时起作用?

When do extra parentheses have an effect, other than on operator precedence?

提问人:TemplateRex 提问时间:6/9/2014 最后编辑:CommunityTemplateRex 更新时间:7/11/2014 访问量:9614

问:

C++ 中的括号用于许多地方:例如,在函数调用和分组表达式中以覆盖运算符优先级。除了非法的额外括号(例如函数调用参数列表周围)之外,C++ 的一般(但不是绝对)规则是额外括号永远不会受到伤害

5.1 主表达式 [expr.prim]

5.1.1 常规 [expr.prim.general]

6 带括号的表达式是主表达式,其类型和 值与包含表达式的值相同。存在感 括号不影响表达式是否为左值。 括号内的表达式可以在完全相同的上下文中使用 作为那些可以使用封闭表达式的地方,并且具有相同的 含义,除非另有说明

问题:除了覆盖基本运算符优先级之外,在哪些上下文中,额外的括号会更改 C++ 程序的含义?

注意:我认为将指针到成员语法限制为不带括号超出了范围,因为它限制了语法,而不是允许两种具有不同含义的语法。同样,在预处理器宏定义中使用括号也可以防止不必要的运算符优先级。&qualified-id

C ++11 语言律师 括号 C++-FAQ

评论

0赞 6/9/2014
“我认为指向成员的指针的 &(qualified-id) 解析是运算符优先级的应用。”如果省略 中的括号,则操作数 is still ,不是吗?&(C::f)&C::f
0赞 TemplateRex 6/9/2014
@hvd :仅当使用显式且其操作数是未括在括号中的限定 ID 时,才会形成指向成员的指针。expr.unary.op/4&
0赞 6/9/2014
是的,那么这与运算符优先级有什么关系呢?(没关系,你编辑的问题已经解决了这个问题。
0赞 TemplateRex 6/9/2014
更新@hvd,我在此问答中将 RHS 与 LHS 混淆了,并且 parens 用于覆盖函数调用在指向成员的指针选择器上的优先级()::*
1赞 Marc van Leeuwen 6/10/2014
我认为您应该更准确地了解哪些情况需要考虑。例如,在类型名称周围加上括号以使其成为 C 样式的强制转换运算符(无论上下文如何),根本不会生成带括号的表达式。另一方面,从技术上讲,我会说 ifwhile 之后的条件是括号表达式,但由于括号是此处语法的一部分,因此不应考虑它们。IMO也不应该是任何情况,如果没有括号,表达式将不再被解析为一个单元,无论是否涉及运算符优先级。

答:

120赞 TemplateRex 6/9/2014 #1

TL;博士

额外的括号在以下上下文中更改 C++ 程序的含义:

  • 防止与参数相关的名称查找
  • 在列表上下文中启用逗号运算符
  • 烦恼解析的歧义解决
  • 推导表达式中的引用性decltype
  • 防止预处理器宏错误

防止与参数相关的名称查找

如该标准的附录 A 中详述的那样,形式的 a 是 ,但不是 ,因此不是 。这意味着与传统形式相比,在表单的函数调用中阻止了与参数相关的名称查找。post-fix expression(expression)primary expressionid-expressionunqualified-id(fun)(arg)fun(arg)

3.4.2 与参数相关的名称查找 [basic.lookup.argdep]

1 当函数调用 (5.2.2) 中的 postfix-expression 是 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), da, b, c, d

5.18 逗号运算符 [expr.comma]

2 在逗号被赋予特殊含义的上下文中,[ 示例:在 函数的参数列表 (5.2.2) 和初始值设定项列表 (8.5) —结束示例 ] 第 5 条中描述的逗号运算符可以 仅出现在括号中。[ 示例:

f(a, (t=3, t+2), c);

有三个参数,其中第二个参数的值为 5。- 结束示例 ]

烦恼解析的歧义解决

向后兼容 C 及其晦涩难懂的函数声明语法可能会导致令人惊讶的解析歧义,称为烦恼解析。从本质上讲,任何可以解析为声明的内容都将被解析为一个声明,即使竞争解析也适用。

6.8 歧义解决 [stmt.ambig]

1 涉及表达式语句的语法存在歧义 和声明:具有函数样式的表达式语句 显式类型转换(5.2.3),因为它最左边的子表达式可以是 与第一个声明符开始的声明没有区别 带有 (.在这些情况下,声明是声明

8.2 歧义解决 [dcl.ambig.res]

1 函数样式之间的相似性引起的歧义 6.8 中提到的 cast 和声明也可能在上下文中发生 的声明。在这种情况下,选择是在函数之间 在参数周围使用一组冗余括号的声明 name 和一个对象声明,其中函数样式强制转换为 初始 化。就像 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

这将声明一个函数 ,其返回类型为 。这 函数数据采用两个参数:datalist<int>

  • 第一个参数名为 。它的类型是 。这 括号是多余的,将被忽略。dataFileistream_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

与类型推导相反,允许推导引用性(左值和右值引用)。规则区分 和 表达式:autodecltypedecltype(e)decltype((e))

7.1.6.2 简单类型说明符 [dcl.type.simple]

4 对于表达式,由 decltype(e) 表示的类型定义为 遵循:e

— if 是带括号的 id-expression 或 不带括号的类成员访问 (5.2.5) 是类型 的实体。如果没有这样的实体,或者如果将 集重载函数,程序格式错误;edecltype(e)ee

— 否则, if 是 xvalue,则为 ,其中 是edecltype(e)T&&Te;

— 否则,如果是左值,则为 ,其中 是类型 之edecltype(e)T&Te;

— 否则,是 的类型。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&

—结束示例] [ 注:7.1.6.4 中指定了确定涉及类型的规则。decltype(auto)

对于初始化表达式的 RHS 中的额外括号,规则具有类似的含义。下面是 C++FAQ相关问答中的一个示例decltype(auto)

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

第一个返回 ,第二个返回 ,这是对局部变量的引用。stringstring &str

防止与预处理器宏相关的错误

预处理器宏在与 C++ 语言本身的交互中存在许多微妙之处,下面列出了最常见的

  • 在宏定义中的宏参数周围使用括号,以避免不必要的运算符优先级(例如,其中产生 9,但如果没有括号,则会产生 6 和#define TIMES(A, B) (A) * (B);TIMES(1 + 2, 2 + 1)(A)(B)
  • 在包含逗号的宏参数周围使用括号:否则将无法编译assert((std::is_same<int, int>::value));
  • 在函数周围使用括号来防止包含的标头中的宏扩展:(具有禁用 ADL 的不良副作用)(min)(a, b)

评论

7赞 Csq 6/9/2014
不会真正改变程序的含义,但最佳实践会影响编译器发出的警告:如果表达式是赋值,则应在 / 中使用额外的括号。例如 -- 警告(你是说吗?),而 -- 没有警告。ifwhileif (a = b)==if ((a = b))
1赞 TemplateRex 6/9/2014
@Csq谢谢,很好的观察,但这是特定编译器的警告,而不是标准强制要求的。我不认为这符合这个问答的语言律师性质。
0赞 Jarod42 6/9/2014
(使用邪恶的 MACRO )是参数依赖名称查找预防的一部分吗?(min)(a, b)min(A, B)
0赞 TemplateRex 6/9/2014
@Jarod42我想是的,但让我们考虑这样和其他邪恶的宏超出了问题的范围:-)
6赞 Jarod42 6/9/2014
@JamesKanze:请注意,OP 和 TemplateRex 是同一个人 ^_^
4赞 Phil Perry 6/9/2014 #2

通常,在编程语言中,“额外”括号意味着它们不会更改语法解析顺序或含义。添加它们是为了澄清顺序(运算符优先级),以便人们阅读代码,它们的唯一作用是稍微减慢编译过程,并减少理解代码时的人为错误(可能加快整个开发过程)。

如果一组括号实际上改变了表达式的解析方式,那么根据定义,它们不是多余的。将非法/无效解析转换为合法解析的括号不是“额外的”,尽管这可能会指出糟糕的语言设计。

评论

2赞 TemplateRex 6/9/2014
没错,这也是 C++ 中的一般规则(请参阅问题中的标准引用),除非另有说明。指出这些“弱点”是这次问答的目的。