Os operadores lógicos em curto-circuito são obrigatórios? E ordem de avaliação?

140

O padrão ANSI exige que os operadores lógicos sofram um curto-circuito, em C ou C ++?

Estou confuso, pois lembro do livro da K&R dizendo que seu código não deveria depender de curto-circuito dessas operações, pois talvez não. Alguém poderia apontar onde, no padrão, as operações lógicas são sempre em curto-circuito? Estou interessado principalmente em C ++, uma resposta também para C seria ótima.

Também me lembro de ler (não lembro onde) que a ordem de avaliação não é estritamente definida; portanto, seu código não deve depender ou assumir funções dentro de uma expressão que serão executadas em uma ordem específica: no final de uma instrução, todas as funções referenciadas terá sido chamado, mas o compilador tem liberdade para selecionar a ordem mais eficiente.

O padrão indica a ordem de avaliação dessa expressão?

if( functionA() && functionB() && functionC() ) cout<<"Hello world";
Joe Pineda
fonte
12
Cuidado: é verdade para os tipos de POD. Mas se você sobrecarregar o operador && ou operator || para uma classe em particular, estes NÃO são repetidos NÃO atalhos. É por isso que é aconselhável que você NÃO defina esses operadores para suas próprias classes.
Martin Iorque
Redefini esses operadores há um tempo atrás, quando criei uma classe que faria algumas operações básicas de álgebra booleana. Provavelmente deve colocar um comentário de aviso "isso destrói curto-circuito e avaliação esquerda-direita!" caso eu esqueça isso. Também sobrecarregado * / + e fez deles seus sinônimos :-)
Joe Pineda
Ter chamadas de função em um bloco if não é uma boa prática de programação. Sempre tenha uma variável declarada que contém o valor de retorno do método e use-o no bloco if.
SR Chaitanya
6
@SRChaitanya Isso não está correto. O que você descreve arbitrariamente como má prática é feito o tempo todo, especialmente com funções que retornam booleanos, como aqui.
Marquês de Lorne

Respostas:

154

Sim, a ordem de curto-circuito e avaliação é necessária para os operadores ||e &&nos padrões C e C ++.

O padrão C ++ diz (deve haver uma cláusula equivalente no padrão C):

1.9.18

Na avaliação das seguintes expressões

a && b
a || b
a ? b : c
a , b

usando o significado interno dos operadores nessas expressões, há um ponto de sequência após a avaliação da primeira expressão (12).

Em C ++, há uma armadilha extra: curto-circuito NÃO se aplica a tipos que sobrecarregam operadores ||e &&.

Nota de rodapé 12: Os operadores indicados neste parágrafo são os operadores internos, conforme descrito na seção 5. Quando um desses operadores é sobrecarregado (seção 13) em um contexto válido, designando uma função de operador definida pelo usuário, a expressão designa uma chamada de função, e os operandos formam uma lista de argumentos, sem um ponto de sequência implícito entre eles.

Geralmente, não é recomendável sobrecarregar esses operadores em C ++, a menos que você tenha um requisito muito específico. Você pode fazê-lo, mas pode quebrar o comportamento esperado no código de outras pessoas, especialmente se esses operadores forem usados ​​indiretamente por meio de modelos de instanciação com o tipo sobrecarregando esses operadores.

Alex B
fonte
3
Não sabia que o curto-circuito não se aplica a operações lógicas sobrecarregadas, isso é complicado. Você pode adicionar uma referência ao padrão ou a uma fonte? Não estou desconfiando de você, só quero saber mais sobre isso.
Joe Pineda
4
Sim, isso é lógico. está agindo como argumentos para o operador && (a, b). é a implementação disso que diz o que acontece.
Johannes Schaub - litb 10/03/09
10
litb: simplesmente não é possível passar b para o operador && (a, b) sem avaliá-lo. E não há como desfazer a avaliação de b porque o compilador não pode garantir que não haja efeitos colaterais.
jmucchiello
2
Eu acho isso triste. Eu teria pensado que, se eu redefinisse os operadores && e || e ainda são totalmente determinísticos , o compilador detectaria isso e manteria sua avaliação em curto-circuito: afinal, a ordem é irrelevante, e eles garantem que não há efeitos colaterais!
Joe Pineda
2
@ Joe: mas o valor de retorno e os argumentos do operador podem mudar de booleano para outra coisa. Eu costumava implementar lógica "especial" com TRÊS valores ("verdadeiro", "falso" e "desconhecido"). O valor de retorno é determinístico, mas o comportamento em curto-circuito não é apropriado.
Alex B
70

A avaliação de curto-circuito e a ordem da avaliação são um padrão semântico obrigatório em C e C ++.

Se não fosse, código como este não seria um idioma comum

   char* pChar = 0;
   // some actions which may or may not set pChar to something
   if ((pChar != 0) && (*pChar != '\0')) {
      // do something useful

   }

A Seção 6.5.13 O operador AND lógico da especificação C99 (link em PDF) diz

(4) Diferentemente do operador binário & bit a bit, o operador && garante a avaliação da esquerda para a direita; existe um ponto de sequência após a avaliação do primeiro operando. Se o primeiro operando for igual a 0, o segundo operando não será avaliado.

Da mesma forma, a seção 6.5.14 O operador lógico OU diz

(4) Ao contrário do bit a bit | operador, o || operador garante avaliação da esquerda para a direita; existe um ponto de sequência após a avaliação do primeiro operando. Se o primeiro operando for comparado a 0, o segundo operando não será avaliado.

Palavras semelhantes podem ser encontradas nos padrões C ++, consulte a seção 5.14 neste rascunho . Como observa o verificador em outra resposta, se você substituir && ou ||, os dois operandos deverão ser avaliados à medida que se tornar uma chamada de função regular.

Paul Dixon
fonte
Ah, o que eu estava procurando! OK, então a ordem de avaliação e o curto-circuito são obrigatórios conforme ANSI-C 99! Eu realmente adoraria ver a referência equivalente ao ANSI-C ++, embora eu tenha quase 99% de diferença.
Joe Pineda
Difícil de encontrar um bom link gratuito para os padrões C ++, vinculei a uma cópia de rascunho que encontrei em alguns sites.
Paul Dixon
Verdadeiro para tipos de POD. Mas se você sobrecarregar o operador && ou o operador || estes não são atalhos.
Martin Iorque
1
Sim, é interessante notar que, para bool, você sempre terá ordem de avaliação garantida e comportamento em curto-circuito. porque você não pode sobrecarregar o operador && para dois tipos internos. você precisa de pelo menos um tipo definido pelo usuário nos operandos para que ele se comporte de maneira diferente.
Johannes Schaub - litb 10/03/09
Eu gostaria de poder aceitar os dois damas e esta resposta. Como estou interessado principalmente em C ++, aceito o outro, mas tenho que admitir que isso também é excelente! Muito obrigado!
Joe Pineda
19

Sim, exige isso (ordem de avaliação e curto-circuito). No seu exemplo, se todas as funções retornarem verdadeiras, a ordem das chamadas será estritamente de functionA, functionB e functionC. Usado para isso como

if(ptr && ptr->value) { 
    ...
}

O mesmo para o operador de vírgula:

// calls a, then b and evaluates to the value returned by b
// which is used to initialize c
int c = (a(), b()); 

Diz-se entre o lado esquerdo e operando à direita de &&, ||, ,e entre o primeiro e segundo / terceiro operando de ?:(operador condicional) é um "ponto de sequência". Quaisquer efeitos colaterais são avaliados completamente antes desse ponto. Então, isso é seguro:

int a = 0;
int b = (a++, a); // b initialized with 1, and a is 1

Observe que o operador de vírgula não deve ser confundido com a vírgula sintática usada para separar as coisas:

// order of calls to a and b is unspecified!
function(a(), b());

O padrão C ++ diz em 5.14/1:

O operador && agrupa da esquerda para a direita. Os operandos são convertidos implicitamente no tipo bool (seção 4). O resultado é verdadeiro se ambos os operandos forem verdadeiros e falsos, caso contrário. Diferentemente de &, && garante a avaliação da esquerda para a direita: o segundo operando não é avaliado se o primeiro operando for falso.

E em 5.15/1:

O || grupos de operadores da esquerda para a direita. Os operandos são implicitamente convertidos em bool (seção 4). Retorna true se um de seus operandos for true e false caso contrário. Ao contrário de |, || garante avaliação da esquerda para a direita; além disso, o segundo operando não será avaliado se o primeiro operando for avaliado como verdadeiro.

Diz para os dois próximos a esses:

O resultado é um bool. Todos os efeitos colaterais da primeira expressão, exceto a destruição de temporários (12.2), ocorrem antes que a segunda expressão seja avaliada.

Além disso, 1.9/18diz

Na avaliação de cada uma das expressões

  • a && b
  • a || b
  • a ? b : C
  • a , b

usando o significado interno dos operadores nessas expressões (5.14, 5.15, 5.16, 5.18), existe um ponto de sequência após a avaliação da primeira expressão.

Johannes Schaub - litb
fonte
10

Direto da boa e velha K&R:

C garante que &&e ||são avaliados da esquerda para a direita - em breve veremos casos em que isso importa.

John T
fonte
3
K&R 2ª edição p40. "Expressões conectadas por && ou || são avaliadas da esquerda para a direita e a avaliação para assim que a verdade ou a falsidade do resultado é conhecida. A maioria dos programas em C se baseia nessas propriedades." Não consigo encontrar o seu texto citado em nenhum lugar do livro. Isso é da 1ª edição extremamente obsoleta? Por favor, esclareça onde você encontrou este texto.
Lundin
1
Ok, você está citando este tutorial antigo . É de 1974 e altamente irrelevante.
Lundin
6

Tenha muito, muito cuidado.

Para tipos fundamentais, esses são operadores de atalho.

Mas se você definir esses operadores para sua própria classe ou tipos de enumeração, eles não serão atalhos. Devido a essa diferença semântica em seu uso nessas diferentes circunstâncias, é recomendável que você não defina esses operadores.

Para os tipos operator &&e operator ||para os fundamentais, a ordem de avaliação é da esquerda para a direita (caso contrário, o atalho seria difícil :-) Mas para os operadores sobrecarregados que você define, estes são basicamente o açúcar sintático para definir um método e, portanto, a ordem de avaliação dos parâmetros é Indefinido.

Martin York
fonte
1
A sobrecarga do operador não tem nada a ver com o tipo sendo POD ou não. Para definir uma função de operador, pelo menos um dos argumentos precisa ser uma classe (ou estrutura ou união) ou uma enumeração ou uma referência a um deles. Ser POD significa que você pode usar o memcpy nele.
Derek Ledbetter
E era isso que eu estava dizendo. Se você sobrecarregar && para a sua classe, é realmente apenas uma chamada de método. Assim, você não pode confiar na ordem de avaliação dos parâmetros. Obviamente, você não pode sobrecarregar && para os tipos de POD.
Martin Iorque
3
Você está usando o termo "tipos de POD" incorretamente. Você pode sobrecarregar && para qualquer estrutura, classe, união ou enumeração, POD ou não. Você não pode sobrecarregar && se os dois lados forem tipos numéricos ou ponteiros.
Derek Ledbetter
Eu estava usando o POD porque (char / int / float etc.) não é um POD agressivo (que é o que você está falando) e geralmente é referido de forma separada ou mais explícita, pois não é um tipo incorporado.
Martin Iorque
2
Então você quis dizer "tipos fundamentais", mas escreveu "tipos de POD"?
Öö Tiib
0

Sua pergunta se resume à precedência e associatividade do operador C ++ . Basicamente, em expressões com vários operadores e sem parênteses, o compilador constrói a árvore de expressões seguindo estas regras.

Por precedência, quando você tem algo parecido A op1 B op2 C, pode agrupar as coisas como (A op1 B) op2 Cou A op1 (B op2 C). Se op1tiver maior precedência do que op2, você obterá a primeira expressão. Caso contrário, você receberá o segundo.

Por associatividade, quando você tem algo parecido A op B op C, pode agrupar novamente como (A op B) op Cou A op (B op C). Se opdeixou a associatividade, terminamos com a primeira expressão. Se tiver associatividade correta, terminamos com a segunda. Isso também funciona para operadores no mesmo nível de precedência.

Nesse caso em particular, &&tem precedência mais alta que ||, portanto, a expressão será avaliada como (a != "" && it == seqMap.end()) || isEven.

A ordem em si é "da esquerda para a direita" no formulário da árvore de expressão. Então, primeiro avaliaremos a != "" && it == seqMap.end(). Se é verdade, toda a expressão é verdadeira; caso contrário, vamos para isEven. O procedimento se repete recursivamente dentro da subexpressão esquerda, é claro.


Boatos interessantes, mas o conceito de precedência tem suas raízes na notação matemática. O mesmo acontece em a*b + c, onde *tem maior precedência do que +.

Ainda mais interessante / obscuro, para uma expressão sem parênteses A1 op1 A2 op2 ... opn-1 An, em que todos os operadores têm a mesma precedência, o número de árvores de expressão binária que poderíamos formar é dado pelos chamados números catalães . Para grandes n, elas crescem extremamente rápido. d

Horia Coman
fonte
Tudo isso está correto, mas é sobre precedência e associatividade do operador, não sobre ordem de avaliação e curto-circuito. Essas são coisas diferentes.
Thomas Padron-McCarthy
0

Se você confia na Wikipedia:

[ &&e ||] são semanticamente distintos dos operadores bit-wise & e | porque eles nunca avaliarão o operando certo se o resultado puder ser determinado apenas a partir da esquerda

C (linguagem de programação)

Sophie Alpert
fonte
11
Por que confiar no wiki quando temos um padrão!
Martin Iorque
1
Se você confia na Wikipedia, 'Wikipedia não é um recurso confiável' .
Marquês de Lorne
Isso é verdade até o momento, mas incompleto, pois os operadores sobrecarregados em C ++ não estão em curto-circuito.
Thomas Padron-McCarthy