除运算符优先级外,什么时候附加括号什么时候起作用?


91

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

5.1主表达式[expr.prim]

5.1.1常规[expr.prim.general]

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

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

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


“我认为指向成员的指针的&(qualified-id)分辨率是运算符优先级的一种应用。” - 这是为什么?如果您省略括号&(C::f),的操作数&仍为C::f,不是吗?

@hvd expr.unary.op/4:仅当使用显式&且其操作数是未括在括号中的限定ID 时,才会形成指向成员的指针。
TemplateRex

是的,那和运算符优先级有什么关系?(没关系,您编辑过的问题将解决此问题。)

@hvd更新,在此问答中我将RHS与LHS混淆了,在这里使用了括号来覆盖函数调用()在指针到成员选择器上的优先级::*
TemplateRex

1
我认为您应该更精确地确定哪些情况需要考虑。例如,在类型名称周围加上括号以使其成为C样式强制转换运算符(无论上下文如何),根本不会在括号中添加表达式。另一方面,从技术上讲,ifwhile之后的条件是带括号的表达式,但是由于括号是语法的一部分,因此不应考虑它们。国际海事组织也不应该在没有括号的情况下将表达式不再解析为一个单元,无论是否涉及到运算符优先级。
Marc van Leeuwen 2014年

Answers:


112

TL; DR

在以下情况下,多余的括号会更改C ++程序的含义:

  • 防止依赖于参数的名称查找
  • 在列表上下文中启用逗号运算符
  • 令人烦恼的解析度的歧义解决
  • 推论decltype表达的参考性
  • 防止预处理器宏错误

防止依赖于参数的名称查找

正如该标准的附录A所详细说明post-fix expression的,形式(expression)的a是a primary expression,但不是an id-expression,因此不是an unqualified-id。这意味着(fun)(arg)与常规形式相比,在形式的函数调用中可以防止依赖于参数的名称查找fun(arg)

3.4.2依赖于参数的名称查找[basic.lookup.argdep]

1当函数调用(5.2.2)中的postfix-expression是unqualified-id时,可以搜索在通常的非限定查找(3.4.1)中未考虑的其他命名空间,并在这些命名空间中命名空间范围的朋友函数或可能会发现不可见的功能模板声明(11.3)。对搜索的这些修改取决于自变量的类型(对于模板模板自变量,则为模板自变量的名称空间)。[示例:

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中提到的声明之间的相似性也可能导致歧义。在这种情况下,选择是在带有围绕参数名称的一对多余括号的函数声明和以函数样式作为初始值设定项的对象声明之间进行选择。就像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
}

—结束示例]

最著名的例子是“ 令人烦恼的解析”,这是斯科特·迈耶斯(Scott Meyers)在他的有效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

十进制类型说明符的操作数是未求值的操作数(第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相关问答中的一个示例

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)和的情况下会产生6的优先级)(B)
  • 在内部带有逗号的宏参数周围使用括号:assert((std::is_same<int, int>::value));否则将无法编译
  • 在函数周围使用圆括号来防止包含的标头中的宏扩展:((min)(a, b)具有也会禁用ADL的有害副作用)

7
并没有真正改变程序的含义,而是最佳实践,并且会影响编译器发出的警告:如果表达式是赋值,则应在if/中使用额外的括号while。例如if (a = b)-警告(您的意思是==?),而if ((a = b))-没有警告。
Csq 2014年

@Csq,谢谢,很好的观察,但这是特定编译器的警告,并非标准的要求。我认为这不符合本问答活动的语言律师性质。
TemplateRex

是否(min)(a, b)(与邪恶MACRO min(A, B))是参数相关的名称查找预防的一部分吗?
Jarod42

@ Jarod42我想是的,但是让我们考虑这样的和其他邪恶的宏不在问题的范围内:-)
TemplateRex

5
@JamesKanze:请注意,OP和TemplateRex是同一个人^ _ ^
Jarod42

4

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

如果一组括号实际上改变了表达式的解析方式,那么按照定义,它们并不是多余的。将非法/无效分析变成合法分析的括号不是“多余”的,尽管这可能表明语言设计不佳。


2
完全一样,这也是C ++的通用规则(请参阅问题中的标准报价),除非另有说明。指出这些“弱点”是本问答的目的。
TemplateRex
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.