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

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

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

5.1.1 Общие [expr.prim.general]

6 Заключенное в скобки выражение — это первичное выражение, тип и
значения идентичны значениям вложенного выражения. Присутствие
скобок не влияет на то, является ли выражение lvalue.
Заключенное в скобки выражение может использоваться в точно таких же контекстах
как те, где можно использовать вложенное выражение, и с тем же
имея в виду, если не указано иное.

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

НОТА: Я считаю ограничение указатель на член синтаксис для &qualified-id без скобок, чтобы выйти за рамки, потому что это ограничивает синтаксис вместо того, чтобы разрешить два синтаксиса с разными значениями. Точно так же использование круглые скобки внутри макроопределений препроцессора также защищает от нежелательного приоритета оператора.

84

Решение

TL; DR

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

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

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

Как подробно описано в Приложении А к Стандарту, post-fix expression формы (expression) это primary expression, но не id-expressionи, следовательно, не unqualified-id, Это означает, что зависимый от аргумента поиск имени предотвращается в вызовах функций вида (fun)(arg) по сравнению с обычной формой fun(arg),

3.4.2 Поиск имени в зависимости от аргумента [basic.lookup.argdep]

1 Когда Постфиксное выражение в вызове функции (5.2.2) является
неквалифицированный-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), d в таком контексте можно включить оператор запятой по сравнению с обычной формой a, 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
}

— конец примера]

Известным примером этого является Most 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>,
Функция data принимает два параметра:

  • Первый параметр называется 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 позволяет выводить ссылки (ссылки на lvalue и rvalue). Правила различают 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 а также это связано Q&

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, но даст 6 без скобок вокруг (A) а также (B)
  • использование скобок вокруг аргументов макроса с запятыми внутри: assert((std::is_same<int, int>::value)); который иначе не скомпилируется
  • использование скобок вокруг функции для защиты от раскрытия макроса во включенных заголовках: (min)(a, b) (с нежелательным побочным эффектом от отключения ADL)
104

Другие решения

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

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

3