Quando os parênteses extras têm efeito, exceto na precedência do operador?

91

Parênteses em C ++ são usados ​​em muitos lugares: por exemplo, em chamadas de função e expressões de agrupamento para substituir a precedência do operador. Além de parênteses extras ilegais (como em torno de listas de argumentos de chamadas de função), uma regra geral -mas não absoluta- de C ++ é que parênteses extras nunca fazem mal :

5.1 Expressões primárias [expr.prim]

5.1.1 Geral [expr.prim.general]

6 Uma expressão entre parênteses é uma expressão primária cujo tipo e valor são idênticos aos da expressão incluída. A presença de parênteses não afeta se a expressão é um lvalue. A expressão entre parênteses pode ser usada exatamente nos mesmos contextos em que a expressão incluída pode ser usada, e com o mesmo significado, exceto quando indicado de outra forma .

Pergunta : em quais contextos os parênteses extras mudam o significado de um programa C ++, exceto sobrepor a precedência do operador básico?

NOTA : Eu considero a restrição da sintaxe de ponteiro para membro&qualified-id sem parênteses fora do escopo porque restringe a sintaxe em vez de permitir duas sintaxes com significados diferentes. Da mesma forma, o uso de parênteses dentro das definições de macro do pré-processador também protege contra precedência de operador indesejada.

TemplateRex
fonte
"Considero a resolução & (id-qualificada) de ponteiro para membro uma aplicação de precedência de operador." -- Por que é que? Se você omitir os parênteses &(C::f), o operando de &permanece C::f, não é?
@hvd expr.unary.op/4: Um ponteiro para membro é formado apenas quando um explícito &é usado e seu operando é um id qualificado não colocado entre parênteses.
TemplateRex
Certo, então o que isso tem a ver com a precedência do operador? (
@hvd atualizado, eu estava confundindo o RHS com o LHS neste Q&A , e lá os parênteses são usados ​​para substituir a precedência da chamada de função ()sobre o seletor de ponteiro para membro::*
TemplateRex
1
Acho que você deveria ser um pouco mais preciso sobre quais casos precisam ser considerados. Por exemplo, parênteses ao redor de um nome de tipo para torná-lo um operador de conversão de estilo C (qualquer que seja o contexto) não fazem uma expressão entre parênteses. Por outro lado, eu diria que tecnicamente a condição após if ou while é uma expressão entre parênteses, mas como os parênteses são parte da sintaxe aqui, eles não devem ser considerados. Nem deveria ser IMO em nenhum caso, onde sem os parênteses a expressão não seria mais analisada como uma única unidade, quer a precedência do operador esteja envolvida ou não.
Marc van Leeuwen de

Respostas:

112

TL; DR

Parênteses extras mudam o significado de um programa C ++ nos seguintes contextos:

  • evitando a procura de nome dependente de argumento
  • habilitando o operador vírgula em contextos de lista
  • resolução de ambigüidade de análises complicadas
  • deduzindo referencia em decltypeexpressões
  • evitando erros de macro do pré-processador

Evitando a procura de nome dependente de argumento

Conforme detalhado no Anexo A da Norma, a post-fix expressiondo formulário (expression)é um primary expression, mas não um id-expressione, portanto, não é um unqualified-id. Isso significa que a pesquisa de nome dependente de argumento é evitada em chamadas de função do formulário em (fun)(arg)comparação com o formato convencional fun(arg).

3.4.2 Pesquisa de nome dependente de argumento [basic.lookup.argdep]

1 Quando a expressão pós-fixada em uma chamada de função (5.2.2) é um id não qualificado , outros namespaces não considerados durante a pesquisa não qualificada usual (3.4.1) podem ser pesquisados, e nesses namespaces, função amiga do escopo de namespace ou declarações de template de função (11.3) não visíveis de outra forma podem ser encontradas. Essas modificações na pesquisa dependem dos tipos de argumentos (e para argumentos de template de template, o namespace do argumento do template). [Exemplo:

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 exemplo]

Ativando o operador vírgula em contextos de lista

O operador vírgula tem um significado especial na maioria dos contextos do tipo lista (argumentos de função e modelo, listas de inicializadores, etc.). Parênteses do formulário a, (b, c), dem tais contextos podem habilitar o operador vírgula em comparação com o formulário regular a, b, c, donde o operador vírgula não se aplica.

5.18 Operador vírgula [expr.comma]

2 Em contextos onde a vírgula recebe um significado especial, [Exemplo: em listas de argumentos para funções (5.2.2) e listas de inicializadores (8.5) - finalizar exemplo], o operador vírgula, conforme descrito na Cláusula 5, pode aparecer apenas entre parênteses. [Exemplo:

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

tem três argumentos, o segundo dos quais tem o valor 5. —enviar exemplo]

Resolução de ambiguidade de análises incômodas

A compatibilidade com versões anteriores com C e sua sintaxe de declaração de função misteriosa pode levar a surpreendentes ambigüidades de análise, conhecidas como análises complicadas. Essencialmente, qualquer coisa que possa ser analisada como uma declaração será analisada como uma , mesmo que uma análise concorrente também se aplique.

6.8 Resolução de ambigüidade [stmt.ambig]

1 Há uma ambigüidade na gramática envolvendo declarações de expressão e declarações : Uma declaração de expressão com uma conversão de tipo explícita de estilo de função (5.2.3) como sua subexpressão mais à esquerda pode ser indistinguível de uma declaração onde o primeiro declarador começa com um ( . nesses casos, a declaração é uma declaração .

8.2 Resolução de ambigüidade [dcl.ambig.res]

1 A ambigüidade que surge da semelhança entre um elenco de estilo de função e uma declaração mencionada em 6.8 também pode ocorrer no contexto de uma declaração . Nesse contexto, a escolha é entre uma declaração de função com um conjunto redundante de parênteses em torno de um nome de parâmetro e uma declaração de objeto com uma conversão de estilo de função como inicializador. Assim como para as ambigüidades mencionadas em 6.8, a resolução é considerar qualquer constructo que possa ser uma declaração uma declaração . [Nota: Uma declaração pode ser explicitamente eliminada da ambigüidade por uma conversão de estilo não funcional, por um = para indicar a inicialização ou removendo os parênteses redundantes em torno do nome do parâmetro. —Enviar nota] [Exemplo:

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 exemplo]

Um exemplo famoso disso é o mais irritante Parse , um nome popularizado por Scott Meyers no item 6 de seu livro 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

Isso declara uma função,, datacujo tipo de retorno é list<int>. Os dados da função têm dois parâmetros:

  • O primeiro parâmetro é nomeado dataFile. Seu tipo é istream_iterator<int>. Os parênteses ao redor dataFilesão supérfluos e são ignorados.
  • O segundo parâmetro não tem nome. Seu tipo é um ponteiro para a função não pegando nada e retornando um istream_iterator<int>.

Colocar parênteses extras ao redor do primeiro argumento da função (parênteses ao redor do segundo argumento são ilegais) irá resolver a ambigüidade

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

C ++ 11 tem sintaxe de inicializador de chave que permite contornar tais problemas de análise em muitos contextos.

Deduzindo referencia em decltypeexpressões

Em contraste com a autodedução de tipo, decltypepermite que a referência (referências lvalue e rvalue) seja deduzida. As regras distinguem entre expressões decltype(e)e decltype((e)):

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

4 Para uma expressão e, o tipo denotado pordecltype(e) é definido como segue:

- se efor uma expressão de id sem parênteses ou um acesso de membro de classe sem parênteses (5.2.5), decltype(e)é o tipo da entidade nomeada por e. Se não houver tal entidade, ou se enomear um conjunto de funções sobrecarregadas, o programa está malformado;

- caso contrário, se eé um valor x, decltype(e)é T&&, onde Té o tipo de e;

- caso contrário, se eé um lvalue, decltype(e)é T&, onde Té o tipo de e;

- caso contrário, decltype(e)é o tipo de e.

O operando do especificador decltype é um operando não avaliado (Cláusula 5). [Exemplo:

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&

—Enviar exemplo] [Nota: As regras para determinar os tipos de envolvimento decltype(auto)são especificadas em 7.1.6.4. —Enviar nota]

As regras para decltype(auto)têm um significado semelhante para parênteses extras no RHS da expressão de inicialização. Aqui está um exemplo do FAQ de C ++ e este Q&A relacionado

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

O primeiro retorna string, o segundo retorna string &, que é uma referência à variável local str.

Prevenção de erros relacionados à macro do pré-processador

Há uma série de sutilezas com macros de pré-processador em sua interação com a linguagem C ++ adequada, as mais comuns das quais estão listadas abaixo

  • usando parênteses em torno dos parâmetros da macro dentro da definição da macro #define TIMES(A, B) (A) * (B);para evitar a precedência indesejada do operador (por exemplo, em TIMES(1 + 2, 2 + 1)que resulta em 9, mas resultaria em 6 sem os parênteses em volta (A)e(B)
  • usando parênteses em torno de argumentos de macro contendo vírgulas: assert((std::is_same<int, int>::value));que de outra forma não seriam compilados
  • usar parênteses em torno de uma função para proteger contra expansão de macro nos cabeçalhos incluídos: (min)(a, b)(com o efeito colateral indesejado de também desativar o ADL)
TemplateRex
fonte
7
Realmente não muda o significado do programa, mas é a melhor prática e afeta os avisos emitidos pelo compilador: parênteses extras devem ser usados ​​em if/ whilese a expressão for uma atribuição. Por exemplo if (a = b)- aviso (você quis dizer ==?), Enquanto if ((a = b))- nenhum aviso.
Csq de
@Csq obrigado, boa observação, mas isso é um aviso de um compilador específico e não exigido pelo padrão. Eu não acho que isso se encaixa na natureza do advogado de linguagem destas perguntas e respostas.
TemplateRex
Faz (min)(a, b)(com o mal MACRO min(A, B)) é parte da prevenção nome lookup dependente de argumento?
Jarod42
@ Jarod42 Acho que sim, mas vamos considerar essas e outras macros malignas fora do escopo da questão :-)
TemplateRex
5
@JamesKanze: Observe que OP e TemplateRex são a mesma pessoa ^ _ ^
Jarod42
4

Em geral, em linguagens de programação, parênteses "extras" implicam que eles não estão mudando a ordem ou o significado da análise sintática. Eles estão sendo adicionados para esclarecer a ordem (precedência do operador) para o benefício das pessoas que lêem o código, e seu único efeito seria desacelerar um pouco o processo de compilação e reduzir os erros humanos na compreensão do código (provavelmente acelerando o processo geral de desenvolvimento )

Se um conjunto de parênteses realmente muda a maneira como uma expressão é analisada, eles, por definição, não são extras. Parênteses que transformam uma análise ilegal / inválida em legal não são "extras", embora isso possa indicar um design de linguagem pobre.

Phil Perry
fonte
2
exatamente, e esta também é a regra geral em C ++ (consulte a citação padrão na pergunta), exceto quando indicado de outra forma . Apontar esses "pontos fracos" foi o objetivo deste Q&A.
TemplateRex