¿Cuándo tienen efecto los paréntesis adicionales, además de la precedencia de los operadores?

91

Los paréntesis en C ++ se utilizan en muchos lugares: por ejemplo, en llamadas a funciones y expresiones de agrupación para anular la precedencia de los operadores. Aparte de los paréntesis adicionales ilegales (como alrededor de las listas de argumentos de llamadas a funciones), una regla general, pero no absoluta, de C ++ es que los paréntesis adicionales nunca hacen daño :

5.1 Expresiones primarias [expr.prim]

5.1.1 General [expr.prim.general]

6 Una expresión entre paréntesis es una expresión primaria cuyo tipo y valor son idénticos a los de la expresión incluida. La presencia de paréntesis no afecta si la expresión es un valor l. La expresión entre paréntesis se puede utilizar exactamente en los mismos contextos en los que se puede utilizar la expresión entre paréntesis, y con el mismo significado, salvo que se indique lo contrario .

Pregunta : ¿en qué contextos los paréntesis adicionales cambian el significado de un programa C ++, además de anular la precedencia básica del operador?

NOTA : Considero que la restricción de la sintaxis de puntero a miembro&qualified-id sin paréntesis está fuera del alcance porque restringe la sintaxis en lugar de permitir dos sintaxis con diferentes significados. De manera similar, el uso de paréntesis dentro de las definiciones de macros del preprocesador también protege contra la precedencia de operadores no deseada.

TemplateRex
fuente
"Considero que la resolución & (id-calificado) de puntero a miembro es una aplicación de la precedencia del operador". -- ¿Porqué es eso? Si omite los paréntesis &(C::f), el operando de &sigue siendo C::f, ¿no es así?
@hvd expr.unary.op/4: un puntero a miembro solo se forma cuando &se usa un explícito y su operando es un id-calificado que no está entre paréntesis.
TemplateRex
Bien, entonces, ¿qué tiene eso que ver con la precedencia del operador? (No importa, tu pregunta editada aclara eso.)
@hvd actualizado, estaba confundiendo el RHS con el LHS en esta sesión de preguntas y respuestas , y allí los parens se usan para anular la precedencia de la llamada a la función ()sobre el selector de puntero a miembro::*
TemplateRex
1
Creo que debería ser un poco más preciso sobre qué casos deben considerarse. Por ejemplo, los paréntesis alrededor de un nombre de tipo para convertirlo en un operador de conversión de estilo C (sea cual sea el contexto) no hacen una expresión entre paréntesis. Por otro lado, diría que técnicamente la condición after if o while es una expresión entre paréntesis, pero dado que los paréntesis son parte de la sintaxis aquí, no deben considerarse. OMI tampoco debería serlo en ningún caso, donde sin los paréntesis la expresión ya no se analizaría como una sola unidad, ya sea que la precedencia del operador esté involucrada o no.
Marc van Leeuwen

Respuestas:

112

TL; DR

Los paréntesis adicionales cambian el significado de un programa C ++ en los siguientes contextos:

  • evitar la búsqueda de nombres dependientes de argumentos
  • habilitar el operador de coma en contextos de lista
  • resolución de ambigüedad de análisis molestos
  • deducir referencias en decltypeexpresiones
  • prevenir errores de macro del preprocesador

Evitar la búsqueda de nombres dependiente de argumentos

Como se detalla en el Anexo A de la Norma, un post-fix expressiondel formulario (expression)es un primary expression, pero no un id-expression, y por lo tanto no un unqualified-id. Esto significa que la búsqueda de nombres dependientes de argumentos se evita en las llamadas a funciones de la forma en (fun)(arg)comparación con la forma convencional fun(arg).

3.4.2 Búsqueda de nombre dependiente del argumento [basic.lookup.argdep]

1 Cuando la expresión-postfijo en una llamada de función (5.2.2) es un id-no calificado , se pueden buscar otros espacios de nombres no considerados durante la búsqueda no calificada habitual (3.4.1), y en esos espacios de nombres, la función amiga del ámbito de nombres o Se pueden encontrar declaraciones de plantilla de función (11.3) que de otro modo no serían visibles. Estas modificaciones a la búsqueda dependen de los tipos de argumentos (y para los argumentos de plantilla de plantilla, el espacio de nombres del argumento de plantilla). [Ejemplo:

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
}

—Ejemplo final]

Habilitar el operador de coma en contextos de lista

El operador de coma tiene un significado especial en la mayoría de los contextos similares a listas (argumentos de función y plantilla, listas de inicializadores, etc.). Los paréntesis del formulario a, (b, c), den tales contextos pueden habilitar el operador de coma en comparación con el formulario regular a, b, c, ddonde no se aplica el operador de coma.

5.18 Operador de coma [expr.comma]

2 En contextos donde la coma tiene un significado especial, [Ejemplo: en listas de argumentos para funciones (5.2.2) y listas de inicializadores (8.5) —ejemplo final], el operador de coma como se describe en la Cláusula 5 puede aparecer sólo entre paréntesis. [Ejemplo:

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

tiene tres argumentos, el segundo de los cuales tiene el valor 5. —ejemplo final]

Resolución de ambigüedad de análisis molestos

La compatibilidad con versiones anteriores de C y su sintaxis de declaración de función arcana puede conducir a sorprendentes ambigüedades de análisis, conocidas como análisis molestos. Básicamente, todo lo que se pueda analizar como una declaración se analizará como uno , aunque también se aplicaría un análisis de la competencia.

6.8 Resolución de ambigüedad [stmt.ambig]

1 Existe una ambigüedad en la gramática que involucra declaraciones-expresión y declaraciones : Una declaración-expresión con una conversión de tipo explícita de estilo de función (5.2.3) como su subexpresión más a la izquierda puede ser indistinguible de una declaración donde el primer declarador comienza con un ( . En aquellos casos en que la declaración es una declaración .

8.2 Resolución de ambigüedad [dcl.ambig.res]

1 La ambigüedad que surge de la similitud entre un reparto de estilo de función y una declaración mencionada en 6.8 también puede ocurrir en el contexto de una declaración . En ese contexto, la elección es entre una declaración de función con un conjunto redundante de paréntesis alrededor de un nombre de parámetro y una declaración de objeto con una conversión de estilo de función como inicializador. Al igual que para las ambigüedades mencionadas en 6.8, la resolución es considerar cualquier construcción que posiblemente podría ser una declaración como una declaración . [Nota: Una declaración se puede eliminar explícitamente de la ambigüedad mediante una conversión de estilo sin función, con un = para indicar la inicialización o eliminando los paréntesis redundantes alrededor del nombre del parámetro. —End note] [Ejemplo:

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
}

—Ejemplo final]

Un ejemplo famoso de esto es Most Vexing Parse , un nombre popularizado por Scott Meyers en el artículo 6 de su libro Effective 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

Esto declara una función data, cuyo tipo de retorno es list<int>. Los datos de la función toman dos parámetros:

  • El primer parámetro se nombra dataFile. Su tipo es istream_iterator<int>. Los paréntesis alrededor dataFileson superfluos y se ignoran.
  • El segundo parámetro no tiene nombre. Su tipo es puntero para funcionar sin tomar nada y devolver un istream_iterator<int>.

Colocar paréntesis adicionales alrededor del primer argumento de la función (los paréntesis alrededor del segundo argumento son ilegales) resolverá la ambigüedad

list<int> data((istream_iterator<int>(dataFile)), // note new parens
                istream_iterator<int>());          // around first argument
                                                  // to list's constructor

C ++ 11 tiene una sintaxis de inicializador de llaves que permite evitar estos problemas de análisis en muchos contextos.

Deducir referencias en decltypeexpresiones

A diferencia de la autodeducción de tipo, decltypepermite deducir la referencia (referencias lvalue y rvalue). Las reglas distinguen entre expresiones decltype(e)y decltype((e)):

7.1.6.2 Especificadores de tipo simple [dcl.type.simple]

4 Para una expresión e, el tipo denotado pordecltype(e) se define como sigue:

- si ees una expresión-id sin paréntesis o un acceso de miembro de clase sin paréntesis (5.2.5), decltype(e)es el tipo de entidad nombrada por e. Si no existe tal entidad, o si enombra un conjunto de funciones sobrecargadas, el programa está mal formado;

- en caso contrario, si ees un valor x, decltype(e)es T&&, donde Tes el tipo de e;

- de lo contrario, si ees un lvalue, decltype(e)es T&, donde Tes el tipo de e;

- de lo contrario, decltype(e)es el tipo de e.

El operando del especificador decltype es un operando no evaluado (cláusula 5). [Ejemplo:

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)—Ejemplo final ] [Nota: Las reglas para determinar los tipos de participación se especifican en 7.1.6.4. —Nota final]

Las reglas para decltype(auto)tienen un significado similar para paréntesis adicionales en el RHS de la expresión de inicialización. Aquí hay un ejemplo de las preguntas frecuentes de C ++ y estas preguntas y respuestas relacionadas

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

El primero devuelve string, el segundo devuelve string &, que es una referencia a la variable local str.

Prevención de errores relacionados con la macro del preprocesador

Existe una gran cantidad de sutilezas con las macros de preprocesador en su interacción con el lenguaje C ++ propiamente dicho, las más comunes de las cuales se enumeran a continuación.

  • utilizando paréntesis alrededor de los parámetros de macro dentro de la definición de macro #define TIMES(A, B) (A) * (B);para evitar la precedencia de operadores no deseados (por ejemplo, en el TIMES(1 + 2, 2 + 1)que produce 9 pero produciría 6 sin los paréntesis alrededor (A)y(B)
  • usando paréntesis alrededor de los argumentos macro que tienen comas dentro: assert((std::is_same<int, int>::value));que de otra manera no se compilarían
  • usar paréntesis alrededor de una función para proteger contra la expansión de macros en los encabezados incluidos: (min)(a, b)(con el efecto secundario no deseado de deshabilitar también ADL)
TemplateRex
fuente
7
Realmente no cambia el significado del programa, pero las mejores prácticas y afectan las advertencias emitidas por el compilador: se deben usar paréntesis adicionales en if/ whilesi la expresión es una asignación. Por ejemplo if (a = b): advertencia (¿quiso decir ==?), Mientras que if ((a = b)): sin advertencia.
Csq
@Csq gracias, buena observación, pero esa es una advertencia de un compilador en particular y no es un mandato del Estándar. No creo que eso encaje con la naturaleza de abogado de idiomas de esta sesión de preguntas y respuestas.
TemplateRex
¿ (min)(a, b)(With evil MACRO min(A, B)) es parte de la prevención de búsqueda de nombres dependiente de argumentos?
Jarod42
@ Jarod42 Supongo que sí, pero consideremos que tales y otras macros malvadas están fuera del alcance de la pregunta :-)
TemplateRex
5
@JamesKanze: Tenga en cuenta que OP y TemplateRex son la misma persona ^ _ ^
Jarod42
4

En general, en los lenguajes de programación, los paréntesis "extra" implica que están no cambiando el orden de análisis sintáctico o significado. Se están agregando para aclarar el orden (precedencia del operador) en beneficio de las personas que leen el código, y su único efecto sería ralentizar ligeramente el proceso de compilación y reducir los errores humanos en la comprensión del código (probablemente acelerando el proceso de desarrollo general ).

Si un conjunto de paréntesis cambia realmente la forma en que se analiza una expresión, entonces, por definición, no son adicionales. Los paréntesis que convierten un análisis ilegal / inválido en uno legal no son "extra", aunque eso puede indicar un diseño de lenguaje deficiente.

Phil Perry
fuente
2
exactamente, y esta es la regla general en C ++ también (consulte la cita estándar en la pregunta), excepto que se indique lo contrario . Señalar estas "debilidades" fue el propósito de estas preguntas y respuestas.
TemplateRex