Когда дополнительные скобки имеют эффект, отличные от приоритета оператора?

Скобки в С++ используются во многих местах: например, в вызовах функций и группировании выражений для переопределения приоритета оператора. Помимо нелегальных дополнительных круглых скобок (например, вокруг списков аргументов вызова функций), общее, но не абсолютное правило С++ состоит в том, что дополнительные круглые скобки никогда не повреждаются:

5.1 Первичные выражения [expr.prim]

5.1.1 Общие сведения [expr.prim.general]

6 Обозначенное в скобках выражение является первичным выражением, тип и значение идентичны значениям прилагаемого выражения. Присутствие круглых скобок не влияет на то, является ли выражение значением l. Выражение в скобках может использоваться в точно таких же контекстах как те, в которых может использоваться замкнутое выражение, и с тем же значение , если не указано иное.

Вопрос: в каких контекстах дополнительные круглые скобки меняют смысл программы на С++, кроме переопределения основного приоритета оператора?

ПРИМЕЧАНИЕ. Я рассматриваю ограничение синтаксиса указателя на член на &qualified-id без круглых скобок, чтобы быть вне области видимости, потому что он ограничивает синтаксис, а не позволяет использовать два синтаксиса с разными значениями. Точно так же использование круглых скобок внутри макроопределений препроцессора также защищает от нежелательного приоритета оператора.

Ответ 1

TL; DR

Дополнительные круглые скобки изменяют значение программы на С++ в следующих контекстах:

  • предотвращение поиска зависимых от аргументов имен
  • включение оператора запятой в контекстах списка
  • двусмысленное разрешение досадных парсов
  • вывод ссылочной категории в выражениях decltype
  • предупреждение макропроцессора препроцессора

Предотвращение поиска зависимых от аргументов имен

Как подробно описано в Приложении A Стандарта, a post-fix expression формы (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), которые не видны другим образом. Эти изменения в поиск зависит от типов аргументов (и шаблона шаблона аргументы, пространство имен аргумента шаблона). [Пример:

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
}

-end пример]

Включение оператора запятой в контекстах списка

Оператор запятой имеет особое значение в большинстве контекстов (например, аргументы функции и шаблона, списки инициализаторов и т.д.). Скобки формы a, (b, c), d в таких контекстах могут включать запятый оператор по сравнению с регулярной формой a, b, c, d, где оператор запятой не применяется.

5.18 Comma operator [expr.comma]

2 В контекстах, где запятая имеет особое значение, [Пример: в списки аргументов к функциям (5.2.2) и списки инициализаторов (8.5) -end example] оператор запятой, как описано в разделе 5, может появляются только в круглых скобках. [Пример:

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

имеет три аргумента, второй из которых имеет значение 5. -end example ]

Разрешение неоднозначности досадных разбора

Обратная совместимость с синтаксисом объявления C и его тайной функции может привести к неожиданной разборчивости разбора, известной как досадные разборки. По сути, все, что может быть проанализировано как объявление, будет анализироваться как один, даже если будет применен и конкурирующий синтаксический анализ.

6.8 Разрешение неоднозначности [stmt.ambig]

1 В грамматике есть двусмысленность, включающая выражения-выражения и объявления: выражение-оператор с функциональным стилем явное преобразование типа (5.2.3), поскольку его самое левое подвыражение может быть неотличимы от декларации, где начинается первый декларатор с. (). В этих случаях утверждение представляет собой объявление.

8.2 Разрешение неоднозначности [dcl.ambig.res]

1 Неоднозначность, возникающая из-за сходства между функциональным стилем и заявка, упомянутая в 6.8, может также возникать в контексте объявления. В этом контексте выбор между функцией объявление с избыточным набором круглых скобок вокруг параметра имя и объявление объекта с помощью функции-стиля в качестве инициализатор. Так же как и для двусмысленностей, упомянутых в 6.8, разрешение рассматривает любую конструкцию, которая может быть объявить декларацию. [Примечание: объявление может быть явно неоднозначно с помощью неэффективного стиля, с помощью = указать инициализации или удаления избыточных круглых скобок вокруг имя параметра. -end note] [Пример:

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
}

-end пример]

Известным примером этого является Самый Vexing Parse, имя, популяризированное Скоттом Мейерсом в пункте 6 его Эффективная версия STL:

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 constructor

С++ 11 имеет синтаксис синтаксического набора, который позволяет во многих контекстах решать такие проблемы синтаксического анализа.

Выделение референциальности в выражениях decltype

В отличие от вычитания типа auto, decltype позволяет определить ссылочность (ссылки lvalue и rvalue). Правила различают выражения decltype(e) и decltype((e)):

7.1.6.2 Спецификаторы простого типа [dcl.type.simple]

4 Для выражения e, тип, обозначенный decltype(e), определяется как следует:

- если e - это неравномерное id-выражение или unparenthesized class member access (5.2.5), decltype(e) - это тип объекта, названного e. Если такой объект отсутствует или если e называет набор перегруженных функций, программа плохо сформирована;

- в противном случае, если e - значение x, decltype(e) - T&&, где T - тип e;

- в противном случае, если e является lvalue, decltype(e) является T&, где T - тип of 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&

-end example] [Примечание: правила для определения типов, включающих decltype(auto) указаны в 7.1.6.4. -end note]

Правила для decltype(auto) имеют аналогичное значение для дополнительных круглых скобок в RHS инициализирующего выражения. Вот пример из С++ FAQ и этот связанные с 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.

Предотвращение макросов, связанных с макропроцессором

Существует множество тонкостей с макросами препроцессора в их взаимодействии с языком С++, наиболее распространенные из которых перечислены ниже

  • используя круглые скобки вокруг макроопределений внутри определения макроса #define TIMES(A, B) (A) * (B);, чтобы избежать нежелательного приоритета оператора (например, в TIMES(1 + 2, 2 + 1), который дает 9, но даст 6 без круглых скобок вокруг (A) и (B)
  • с помощью круглых скобок вокруг макросов с запятыми внутри: assert((std::is_same<int, int>::value));, которые иначе не скомпилировались
  • с помощью круглых скобок вокруг функции для защиты от макрорасширения в включенных заголовках: (min)(a, b) (с нежелательным побочным эффектом также отключение ADL)

Ответ 2

В общем, в языках программирования "лишние" круглые скобки подразумевают, что они не меняют синтаксический порядок или значение синтаксического анализа. Они добавляются, чтобы уточнить порядок (приоритет оператора) для людей, читающих код, и их единственным эффектом было бы немного замедлить процесс компиляции и уменьшить человеческие ошибки в понимании кода (возможно, ускорить общий процесс разработки).

Если набор круглых скобок фактически изменяет способ анализа, то они по определению не дополнительно. Скобки, которые превращают незаконный/недействительный анализ в юридический, не являются "лишними", хотя это может указывать на плохой дизайн языка.