【问题标题】:When do extra parentheses have an effect, other than on operator precedence?除了对运算符优先级之外,额外的括号何时会产生影响?
【发布时间】:2014-07-29 18:56:15
【问题描述】:

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

5.1 主要表达式 [expr.prim]

5.1.1 通用 [expr.prim.general]

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

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

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

【问题讨论】:

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

标签: c++ c++11 language-lawyer parentheses c++-faq


【解决方案1】:

TL;DR

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

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

防止参数依赖的名称查找

如标准附录 A 中所述,(expression) 形式的 post-fix expressionprimary 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&lt;int&gt;。这 函数数据有两个参数:

  • 第一个参数名为dataFile。它的类型是istream_iterator&lt;int&gt;。这 dataFile 周围的括号是多余的,将被忽略。
  • 第二个参数没有名字。它的类型是指向函数的指针 什么都没有,返回一个istream_iterator&lt;int&gt;

在第一个函数参数周围放置额外的括号(第二个参数周围的括号是非法的)将解决歧义

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 对于表达式edecltype(e)表示的类型定义为 如下:

——如果e 是一个未加括号的 id 表达式或 无括号类成员访问(5.2.5),decltype(e) 是类型 由e 命名的实体。如果没有这样的实体,或者如果e 命名为 一组重载函数,程序格式错误;

——否则, 如果e 是一个xvalue,decltype(e)T&amp;&amp;,其中Te 的类型;

—— 否则,如果e 是左值,则decltype(e)T&amp;,其中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++FAQthis 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 &amp;,这是对局部变量str的引用。

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

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

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

【讨论】:

  • 并没有真正改变程序的含义,而是最佳实践并影响编译器发出的警告:如果表达式是赋值,则应在if/while 中使用额外的括号。例如。 if (a = b) -- 警告(你的意思是==?),而if ((a = b)) -- 没有警告。
  • @Csq 谢谢,很好的观察,但这是特定编译器的警告,而不是标准规定的。我认为这不符合本问答的语言律师性质。
  • (min)(a, b) (with evil MACRO min(A, B)) 是否是依赖于参数的名称查找预防的一部分?
  • @Jarod42 我猜是这样,但让我们认为这样的and other evil macros 超出了问题的范围:-)
  • @JamesKanze:注意OP和TemplateRex是同一个人^_^
【解决方案2】:

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

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

【讨论】:

  • 完全正确,这也是 C++ 中的一般规则(请参阅问题中的标准引用),除非另有说明。指出这些“弱点”是本次问答的目的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-07-25
  • 1970-01-01
  • 2014-09-09
  • 1970-01-01
  • 1970-01-01
  • 2015-11-08
  • 1970-01-01
相关资源
最近更新 更多