Por que essas construções estão usando um comportamento indefinido pré e pós-incremento?

815
#include <stdio.h>

int main(void)
{
   int i = 0;
   i = i++ + ++i;
   printf("%d\n", i); // 3

   i = 1;
   i = (i++);
   printf("%d\n", i); // 2 Should be 1, no ?

   volatile int u = 0;
   u = u++ + ++u;
   printf("%d\n", u); // 1

   u = 1;
   u = (u++);
   printf("%d\n", u); // 2 Should also be one, no ?

   register int v = 0;
   v = v++ + ++v;
   printf("%d\n", v); // 3 (Should be the same as u ?)

   int w = 0;
   printf("%d %d\n", ++w, w); // shouldn't this print 1 1

   int x[2] = { 5, 8 }, y = 0;
   x[y] = y ++;
   printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}
PiX
fonte
12
@ Jarett, não, só precisava de alguns ponteiros para "pontos de sequência". Enquanto trabalhava, encontrei um pedaço de código com i = i ++, pensei: "Isso não está modificando o valor de i". Eu testei e me perguntei o porquê. Desde então, removi esta declaração e a substitui pelo i ++;
PiX
198
Eu acho interessante que todo mundo SEMPRE assuma que perguntas como essa são feitas porque o solicitante deseja USAR o construto em questão. Minha primeira suposição foi de que o PiX sabe que isso é ruim, mas é curioso o porquê do comportamento deles no que quer que o compilador estivesse usando ... .. incluindo JCF (Jump and Catch Fire)
Brian Postow
32
Estou curioso: por que os compiladores não parecem avisar sobre construções como "u = u ++ + ++ u;" se o resultado for indefinido?
Aprenda OpenGL ES
5
(i++)ainda avalia a 1, independentemente de parênteses
de Drew McGowen
2
Tudo o que i = (i++);se pretendia fazer, certamente existe uma maneira mais clara de escrevê-lo. Isso seria verdade mesmo se estivesse bem definido. Mesmo em Java, que define o comportamento de i = (i++);, ainda é um código incorreto. Basta escrever #i++;
Keith Thompson

Respostas:

566

C tem o conceito de comportamento indefinido, ou seja, algumas construções de linguagem são sintaticamente válidas, mas você não pode prever o comportamento quando o código é executado.

Até onde eu sei, o padrão não diz explicitamente por que o conceito de comportamento indefinido existe. Na minha opinião, é simplesmente porque os projetistas de linguagem queriam que houvesse alguma margem de manobra na semântica, em vez de exigir que todas as implementações lidassem com excesso de números inteiros da mesma maneira exata, o que provavelmente imporia custos sérios de desempenho, eles apenas deixaram o comportamento indefinido para que, se você escrever um código que cause excesso de número inteiro, tudo possa acontecer.

Então, com isso em mente, por que esses "problemas"? A linguagem diz claramente que certas coisas levam a um comportamento indefinido . Não há problema, não há "deveria" envolvido. Se o comportamento indefinido for alterado quando uma das variáveis ​​envolvidas for declarada volatile, isso não prova ou altera nada. Está indefinido ; você não pode raciocinar sobre o comportamento.

Seu exemplo mais interessante, aquele com

u = (u++);

é um exemplo de comportamento indefinido em livro-texto (consulte a entrada da Wikipedia sobre pontos de sequência ).

descontrair
fonte
8
@ PiX: As coisas são indefinidas por várias razões possíveis. Eles incluem: não há um "resultado certo" claro, diferentes arquiteturas de máquinas favoreceriam fortemente diferentes resultados, a prática existente não é consistente ou está além do escopo do padrão (por exemplo, quais nomes de arquivos são válidos).
Richard
Apenas para confundir todos, alguns exemplos agora estão bem definidos no C11, por exemplo i = ++i + 1;.
MM
2
Lendo o Padrão e a lógica publicada, fica claro por que o conceito de UB existe. O Padrão nunca teve a intenção de descrever completamente tudo o que uma implementação C deve fazer para ser adequado a qualquer propósito específico (consulte a discussão da regra do "Programa Único"), mas, em vez disso, se baseia no julgamento dos implementadores e no desejo de produzir implementações de qualidade úteis. Uma implementação de qualidade adequada para a programação de sistemas de baixo nível precisará definir o comportamento das ações que não seriam necessárias nos aplicativos de processamento de números de última geração. Em vez de tentar complicar o padrão ... #
687
3
... ao entrar em detalhes extremos sobre quais casos de canto são ou não definidos, os autores da Norma reconheceram que os implementadores devem ter um ritmo melhor para julgar que tipos de comportamento serão necessários pelos tipos de programas que eles devem apoiar . Os compiladores hiper-modernistas fingem que fazer determinadas ações UB pretendia implicar que nenhum programa de qualidade deveria precisar delas, mas o Padrão e a lógica são inconsistentes com essa suposta intenção.
Supercat
1
@jrh: escrevi essa resposta antes de perceber como fora de controle a filosofia hipermodernista. O que me incomoda é a progressão de "Não precisamos reconhecer oficialmente esse comportamento porque as plataformas onde é necessário podem suportá-lo de qualquer maneira" para "Podemos remover esse comportamento sem fornecer uma substituição utilizável porque nunca foi reconhecido e, portanto, qualquer código precisando que estivesse quebrado ". Muitos comportamentos deveriam ter sido depreciados há muito tempo em favor de substituições que eram de todo modo melhores , mas isso exigiria reconhecer sua legitimidade.
supercat
78

Basta compilar e desmontar sua linha de código, se você estiver tão inclinado a saber exatamente como é obter o que está recebendo.

É isso que recebo na minha máquina, junto com o que acho que está acontecendo:

$ cat evil.c
void evil(){
  int i = 0;
  i+= i++ + ++i;
}
$ gcc evil.c -c -o evil.bin
$ gdb evil.bin
(gdb) disassemble evil
Dump of assembler code for function evil:
   0x00000000 <+0>:   push   %ebp
   0x00000001 <+1>:   mov    %esp,%ebp
   0x00000003 <+3>:   sub    $0x10,%esp
   0x00000006 <+6>:   movl   $0x0,-0x4(%ebp)  // i = 0   i = 0
   0x0000000d <+13>:  addl   $0x1,-0x4(%ebp)  // i++     i = 1
   0x00000011 <+17>:  mov    -0x4(%ebp),%eax  // j = i   i = 1  j = 1
   0x00000014 <+20>:  add    %eax,%eax        // j += j  i = 1  j = 2
   0x00000016 <+22>:  add    %eax,-0x4(%ebp)  // i += j  i = 3
   0x00000019 <+25>:  addl   $0x1,-0x4(%ebp)  // i++     i = 4
   0x0000001d <+29>:  leave  
   0x0000001e <+30>:  ret
End of assembler dump.

(... suponho que a instrução 0x00000014 fosse algum tipo de otimização do compilador?)

badp
fonte
como obtenho o código da máquina? Eu uso Dev C ++, e eu brinquei opção 'Code Generation' em configurações do compilador com, mas ir nenhuma saída extra de arquivo ou qualquer saída do console
bad_keypoints
5
@ronnieaka gcc evil.c -c -o evil.bine gdb evil.bindisassemble evil, ou o que os equivalentes do Windows daqueles são :)
badp
21
Esta resposta realmente não aborda a questão de Why are these constructs undefined behavior?.
Shafik Yaghmour
9
Como um aparte, será mais fácil compilar em assembly (with gcc -S evil.c), que é tudo o que é necessário aqui. Montar e desmontar é apenas uma maneira indireta de fazê-lo.
27415 Kat
50
Para constar, se por algum motivo você estiver se perguntando o que uma determinada construção faz - e especialmente se houver alguma suspeita de que possa ser um comportamento indefinido - o antigo conselho de "apenas tentar com seu compilador e ver" é potencialmente bastante perigoso. Você aprenderá, na melhor das hipóteses, o que ele faz nesta versão do seu compilador, nessas circunstâncias, hoje . Você não aprenderá muito, se houver algo sobre o que é garantido que ele fará. Em geral, "tente com o seu compilador" leva a programas não portáveis ​​que funcionam apenas com o seu compilador.
Steve Summit
64

Eu acho que as partes relevantes do padrão C99 são 6.5 Expressions, §2

Entre o ponto de sequência anterior e o próximo, um objeto deve ter seu valor armazenado modificado no máximo uma vez pela avaliação de uma expressão. Além disso, o valor anterior deve ser lido apenas para determinar o valor a ser armazenado.

e 6.5.16 Operadores de atribuição, §4:

A ordem de avaliação dos operandos não é especificada. Se for feita uma tentativa de modificar o resultado de um operador de atribuição ou de acessá-lo após o próximo ponto de sequência, o comportamento será indefinido.

Christoph
fonte
2
O que precede implica que 'i = i = 5; "seria um comportamento indefinido?
supercat
1
@supercat, tanto quanto eu sei i=i=5é também um comportamento indefinido
Dhein
2
@Zaibis: A lógica que eu gosto de usar para a maioria dos lugares aplica-se à teoria de que uma plataforma de processadores mutli poderia implementar algo como A=B=5;"Bloqueio de gravação A; Bloqueio de gravação B; Armazene 5 a A; loja 5 a B; desbloqueie B ; Desbloqueie A; "e uma instrução como C=A+B;" Bloqueio de leitura A; Bloqueio de leitura B; Cálculo A + B; Desbloqueie A e B; Bloqueio de gravação C; Armazene o resultado; Desbloqueie C; ". Isso garantiria que, se um encadeamento ocorresse A=B=5;enquanto outro, C=A+B;o último encadeamento veria ambas as gravações como tendo ocorrido ou nenhuma. Potencialmente uma garantia útil. Se um thread fez I=I=5;, no entanto, ... #
2333
1
... e o compilador não notou que as duas gravações estavam no mesmo local (se um ou ambos os lvalues ​​envolverem ponteiros, que podem ser difíceis de determinar), o código gerado poderá entrar em conflito. Eu não acho que nenhuma implementação do mundo real implemente esse bloqueio como parte de seu comportamento normal, mas seria permitido de acordo com o padrão, e se o hardware pudesse implementar esses comportamentos de forma barata, isso seria útil. No hardware atual, esse comportamento seria muito caro para implementar como padrão, mas isso não significa que sempre seria assim.
supercat 23/09
1
@ supercat, mas a regra de acesso ao ponto de sequência da c99 por si só não seria suficiente para declara-la como comportamento indefinido? Portanto, não importa o que tecnicamente o hardware possa implementar?
Dhein
55

A maioria das respostas aqui citadas do padrão C enfatiza que o comportamento dessas construções é indefinido. Para entender por que o comportamento dessas construções não é definido , vamos entender esses termos primeiro à luz do padrão C11:

Sequenciado: (5.1.2.3)

Dadas duas avaliações Ae B, se Ajá tiver sido sequenciada anteriormente B, a execução de Aprecederá a execução de B.

Sem seqüência:

Se Anão for sequenciado antes ou depois B, Ae não será seqüenciado B.

As avaliações podem ser uma de duas coisas:

  • cálculos de valores , que calculam o resultado de uma expressão; e
  • efeitos colaterais , que são modificações de objetos.

Ponto de sequência:

A presença de um ponto de sequência entre a avaliação de expressões Ae Bimplica que todo cálculo de valor e efeito colateral associado Asejam sequenciados antes de cada cálculo de valor e efeito colateral associado B.

Agora voltando à pergunta, para as expressões como

int i = 1;
i = i++;

padrão diz que:

6.5 Expressões:

Se um efeito secundário de um objecto de escalar é unsequenced relativa a um ou outro efeito secundário diferente no mesmo objecto escalar ou um cálculo do valor com o valor do mesmo objecto escalar, o comportamento é indefinido . [...]

Portanto, a expressão acima invoca UB porque dois efeitos colaterais no mesmo objeto isão sem seqüência em relação um ao outro. Isso significa que não é sequenciado se o efeito colateral por atribuição iserá realizado antes ou depois do efeito colateral por ++.
Dependendo da atribuição ocorrer antes ou depois do incremento, resultados diferentes serão produzidos e esse é o caso de comportamento indefinido .

Vamos renomear a iesquerda da atribuição be ile à direita da atribuição (na expressão i++) be ir, então a expressão será como

il = ir++     // Note that suffix l and r are used for the sake of clarity.
              // Both il and ir represents the same object.  

Um ponto importante em relação ao ++operador Postfix é que:

só porque o resultado ++da variável não significa que o incremento acontece tarde . O incremento pode ocorrer assim que o compilador gostar , desde que o compilador garanta que o valor original seja usado .

Isso significa que a expressão il = ir++pode ser avaliada como

temp = ir;      // i = 1
ir = ir + 1;    // i = 2   side effect by ++ before assignment
il = temp;      // i = 1   result is 1  

ou

temp = ir;      // i = 1
il = temp;      // i = 1   side effect by assignment before ++
ir = ir + 1;    // i = 2   result is 2  

resultando em dois resultados diferentes 1e 2que depende da sequência de efeitos colaterais por atribuição ++e, portanto, chama UB.

haccks
fonte
52

O comportamento não pode realmente ser explicado porque invoca o comportamento não especificado quanto o indefinido ; portanto, não podemos fazer previsões gerais sobre esse código, embora se você ler o trabalho de Olve Maudal , como Deep C e Unespecified and Undefined, às vezes você poderá obter bons resultados. palpites em casos muito específicos com um compilador e um ambiente específicos, mas não faça isso nem perto da produção.

Então, passando para um comportamento não especificado , no rascunho da seção padrão da c99 , o 6.5parágrafo 3 diz ( ênfase minha ):

O agrupamento de operadores e operandos é indicado pela sintaxe.74) Exceto conforme especificado posteriormente (para os operadores de chamada de função (), &&, ||,?: E vírgula), a ordem de avaliação das subexpressões e a ordem em quais efeitos colaterais ocorrem não são especificados.

Então, quando temos uma linha como esta:

i = i++ + ++i;

não sabemos se serão i++ou ++inão avaliados primeiro. Isso é principalmente para fornecer ao compilador melhores opções de otimização .

Nós também têm um comportamento indefinido aqui também desde que o programa está a modificar variáveis ( i, u, etc ..) mais de uma vez entre os pontos de sequência . Do 6.5parágrafo de seção padrão de desenho 2 ( ênfase minha ):

Entre o ponto de sequência anterior e o próximo, um objeto deve ter seu valor armazenado modificado no máximo uma vez pela avaliação de uma expressão. Além disso, o valor anterior deve ser lido apenas para determinar o valor a ser armazenado .

cita os seguintes exemplos de código como indefinidos:

i = ++i + 1;
a[i++] = i; 

Em todos esses exemplos, o código está tentando modificar um objeto mais de uma vez no mesmo ponto de sequência, que terminará com o ; em cada um desses casos:

i = i++ + ++i;
^   ^       ^

i = (i++);
^    ^

u = u++ + ++u;
^   ^       ^

u = (u++);
^    ^

v = v++ + ++v;
^   ^       ^

O comportamento não especificado é definido no rascunho da norma c99 na seção 3.4.4como:

uso de um valor não especificado ou outro comportamento em que esta Norma Internacional ofereça duas ou mais possibilidades e não imponha requisitos adicionais sobre os quais é escolhido em qualquer instância

O comportamento indefinido é definido na seção 3.4.3como:

comportamento, mediante o uso de uma construção de programa não transportável ou incorreta ou de dados errados, para os quais esta Norma Internacional não impõe requisitos

e observa que:

O possível comportamento indefinido varia de ignorar a situação completamente com resultados imprevisíveis, se comportar durante a tradução ou a execução do programa de maneira documentada característica do ambiente (com ou sem a emissão de uma mensagem de diagnóstico), até o término de uma tradução ou execução (com a emissão de uma mensagem de diagnóstico).

Shafik Yaghmour
fonte
33

Outra maneira de responder a isso, em vez de se atolar em detalhes misteriosos de pontos de sequência e comportamento indefinido, é simplesmente perguntar: o que eles deveriam significar? O que o programador estava tentando fazer?

O primeiro fragmento perguntado sobre, i = i++ + ++ié claramente insano no meu livro. Ninguém jamais escreveria isso em um programa real, não é óbvio o que ele faz, não há algoritmo concebível que alguém possa estar tentando codificar que resultaria nessa sequência artificial de operações. E como não é óbvio para você e para mim o que deve fazer, tudo bem no meu livro se o compilador também não puder descobrir o que deve fazer.

O segundo fragmento,, i = i++é um pouco mais fácil de entender. Alguém está claramente tentando incrementar i e atribuir o resultado de volta a i. Mas existem algumas maneiras de fazer isso em C. A maneira mais básica de adicionar 1 a i e atribuir o resultado de volta a i é a mesma em quase qualquer linguagem de programação:

i = i + 1

C, é claro, tem um atalho útil:

i++

Isso significa "adicione 1 a i e atribua o resultado de volta a i". Portanto, se construirmos uma mistura de dois, escrevendo

i = i++

o que realmente estamos dizendo é "adicione 1 a i e atribua o resultado de volta a i, e atribua o resultado de volta a i". Estamos confusos, por isso não me incomoda muito se o compilador ficar confuso também.

Realisticamente, a única vez em que essas expressões malucas são escritas é quando as pessoas as usam como exemplos artificiais de como o ++ deve funcionar. E, claro, é importante entender como o ++ funciona. Mas uma regra prática para usar ++ é: "Se não for óbvio o que significa uma expressão usando ++, não a escreva".

Costumávamos passar inúmeras horas no comp.lang.c discutindo expressões como essas e por que elas são indefinidas. Duas das minhas respostas mais longas, que tentam realmente explicar o porquê, estão arquivadas na Web:

Veja também questionar 3.8 eo resto das perguntas em seção 3 da lista C FAQ .

Steve Summit
fonte
1
Uma pegadinha bastante desagradável em relação ao Comportamento indefinido é que, enquanto costumava ser seguro em 99,9% dos compiladores *p=(*q)++;, if (p!=q) *p=(*q)++; else *p= __ARBITRARY_VALUE;isso significa que esse não é mais o caso. C hiper-moderno exigiria escrever algo como a última formulação (embora não exista uma maneira padrão de indicar que o código não se importa *p) para atingir o nível de compiladores de eficiência usados ​​para fornecer ao primeiro (a elsecláusula é necessária para permitir o compilador otimiza o ifque alguns compiladores mais recentes exigiriam).
precisa
@ supercat Agora eu acredito que qualquer compilador que seja "inteligente" o suficiente para executar esse tipo de otimização também deve ser inteligente o suficiente para espiar as assertdeclarações, para que o programador possa preceder a linha em questão com um simples assert(p != q). (É claro, fazer esse curso também exigiria reescrever <assert.h>para não excluir as afirmações diretamente nas versões sem depuração, mas transformá-las em algo __builtin_assert_disabled()que o compilador possa ver e depois não emitir código para.)
Steve Summit,
25

Muitas vezes, essa pergunta é vinculada como uma duplicata de questões relacionadas a códigos como

printf("%d %d\n", i, i++);

ou

printf("%d %d\n", ++i, i++);

ou variantes semelhantes.

Embora esse também seja um comportamento indefinido, como já declarado, há diferenças sutis quando printf()envolvido ao comparar com uma declaração como:

x = i++ + i++;

Na declaração a seguir:

printf("%d %d\n", ++i, i++);

a ordem de avaliação dos argumentos nãoprintf() é especificada . Isso significa expressões i++e ++ipode ser avaliado em qualquer ordem. O padrão C11 tem algumas descrições relevantes sobre isso:

Anexo J, comportamentos não especificados

A ordem na qual o designador de função, argumentos e subexpressões dentro dos argumentos são avaliados em uma chamada de função (6.5.2.2).

3.4.4, comportamento não especificado

Uso de um valor não especificado ou outro comportamento em que esta Norma Internacional ofereça duas ou mais possibilidades e não imponha outros requisitos sobre os quais é escolhido em qualquer instância.

EXEMPLO Um exemplo de comportamento não especificado é a ordem na qual os argumentos para uma função são avaliados.

O comportamento não especificado em si NÃO é um problema. Considere este exemplo:

printf("%d %d\n", ++x, y++);

Isso também tem comportamento não especificado porque a ordem de avaliação ++xe y++não é especificada. Mas é uma declaração perfeitamente legal e válida. Não comportamento indefinido nesta declaração. Porque as modificações ( ++xe y++) são feitas em objetos distintos .

O que torna a seguinte declaração

printf("%d %d\n", ++i, i++);

como comportamento indefinido é o fato de que essas duas expressões modificam o mesmo objeto isem um ponto de sequência intermediário .


Outro detalhe é que a vírgula envolvida na chamada printf () é um separador , não o operador de vírgula .

Essa é uma distinção importante porque o operador de vírgula introduz um ponto de sequência entre a avaliação de seus operandos, o que torna legal o seguinte:

int i = 5;
int j;

j = (++i, i++);  // No undefined behaviour here because the comma operator 
                 // introduces a sequence point between '++i' and 'i++'

printf("i=%d j=%d\n",i, j); // prints: i=7 j=6

O operador de vírgula avalia seus operandos da esquerda para a direita e produz apenas o valor do último operando. Assim, em j = (++i, i++);, ++iincrementos ipara 6e i++rendimentos de valor antigo da i( 6) que é atribuído a j. Em seguida, itorna-se 7devido ao pós-incremento.

Portanto, se a vírgula na chamada de função fosse um operador de vírgula,

printf("%d %d\n", ++i, i++);

não será um problema. Mas ele invoca um comportamento indefinido porque a vírgula aqui é um separador .


Para quem é novo no comportamento indefinido, se beneficiaria da leitura do que todo programador C deve saber sobre o comportamento indefinido para entender o conceito e muitas outras variantes de comportamento indefinido em C.

Esta publicação: Comportamento indefinido, não especificado e definido pela implementação também é relevante.

PP
fonte
Essa sequência int a = 10, b = 20, c = 30; printf("a=%d b=%d c=%d\n", (a = a + b + c), (b = b + b), (c = c + c));parece fornecer um comportamento estável (avaliação do argumento da direita para a esquerda no gcc v7.3.0; resultado "a = 110 b = 40 c = 60"). É porque as atribuições são consideradas como 'declarações completas' e, assim, introduzem um ponto de sequência? Isso não deveria resultar na avaliação de argumentos / declarações da esquerda para a direita? Ou é apenas manifestação de comportamento indefinido?
Kavadias 17/10/18
@kavadias Essa declaração printf envolve um comportamento indefinido, pelo mesmo motivo explicado acima. Você está escrevendo be cno terceiro e quarto argumentos, respectivamente, e lendo no segundo argumento. Mas não há sequência entre essas expressões (segundo, terceiro e quarto argumentos). O gcc / clang tem uma opção -Wsequence-pointque pode ajudar a encontrá-los também.
PP
23

Embora seja improvável que qualquer compilador e processador o faça, seria legal, sob o padrão C, que o compilador implementasse "i ++" com a sequência:

In a single operation, read `i` and lock it to prevent access until further notice
Compute (1+read_value)
In a single operation, unlock `i` and store the computed value

Embora eu ache que nenhum processador suporte o hardware para permitir que isso seja feito com eficiência, é fácil imaginar situações em que esse comportamento facilitaria o código multiencadeado (por exemplo, garantiria que, se dois threads tentassem executar o procedimento acima). sequência simultaneamente, iseria incrementado em dois) e não é totalmente inconcebível que algum processador futuro forneça um recurso parecido com esse.

Se o compilador escrever i++como indicado acima (legal sob o padrão) e intercalar as instruções acima em toda a avaliação da expressão geral (também legal), e se não perceber que uma das outras instruções ocorreu para acessar i, seria possível (e legal) para o compilador gerar uma sequência de instruções que causariam um conflito. Certamente, um compilador quase certamente detectaria o problema no caso em que a mesma variável ié usada nos dois lugares, mas se uma rotina aceitar referências a dois ponteiros pe q, and use (*p)e (*q)na expressão acima (em vez de usariduas vezes) não seria necessário que o compilador reconhecesse ou evitasse o conflito que ocorreria se o endereço do mesmo objeto fosse passado para ambos .p eq

supercat
fonte
16

Embora a sintaxe das expressões como a = a++ou a++ + a++seja legal, o comportamento dessas construções seja indefinido porque uma regra no padrão C não é obedecida. C99 6.5p2 :

  1. Entre o ponto de sequência anterior e o próximo, um objeto deve ter seu valor armazenado modificado no máximo uma vez pela avaliação de uma expressão. [72] Além disso, o valor anterior deve ser lido apenas para determinar o valor a ser armazenado [73]

Com a nota 73, esclarecendo ainda mais que

  1. Este parágrafo renderiza expressões de declaração indefinidas, como

    i = ++i + 1;
    a[i++] = i;

    enquanto permite

    i = i + 1;
    a[i] = i;

Os vários pontos de sequência estão listados no anexo C de C11 (e C99 ):

  1. A seguir, são apresentados os pontos de sequência descritos em 5.1.2.3:

    • Entre as avaliações do designador de função e os argumentos reais em uma chamada de função e a chamada real. (6.5.2.2)
    • Entre as avaliações do primeiro e do segundo operandos dos seguintes operadores: AND lógico (&) (6.5.13); OR lógico || (6.5.14); vírgula, (6.5.17).
    • Entre as avaliações do primeiro operando do condicional? : operador e qualquer um dos segundo e terceiro operandos avaliado (6.5.15).
    • O fim de um declarador completo: declarators (6.7.6);
    • Entre a avaliação de uma expressão completa e a próxima expressão completa a ser avaliada. A seguir, são apresentadas expressões completas: um inicializador que não faz parte de um literal composto (6.7.9); a expressão em uma declaração de expressão (6.8.3); a expressão de controle de uma instrução de seleção (if ou switch) (6.8.4); a expressão controladora de uma declaração while ou do (6.8.5); cada uma das expressões (opcionais) de uma instrução for (6.8.5.3); a expressão (opcional) em uma instrução de retorno (6.8.6.4).
    • Imediatamente antes do retorno de uma função de biblioteca (7.1.4).
    • Após as ações associadas a cada especificador de conversão de função de entrada / saída formatado (7.21.6, 7.29.2).
    • Imediatamente antes e imediatamente após cada chamada para uma função de comparação, e também entre qualquer chamada para uma função de comparação e qualquer movimento dos objetos passados ​​como argumentos para essa chamada (7.22.5).

A redação do mesmo parágrafo em C11 é:

  1. Se um efeito colateral em um objeto escalar não for relacionado em relação a um efeito colateral diferente no mesmo objeto escalar ou a uma computação de valor usando o valor do mesmo objeto escalar, o comportamento será indefinido. Se houver várias ordenações permitidas das subexpressões de uma expressão, o comportamento será indefinido se ocorrer um efeito colateral indesejado em qualquer uma das ordenações.84)

Você pode detectar esses erros em um programa, por exemplo, usando uma versão recente do GCC com -Walle -Werror, em seguida, o GCC se recusará totalmente a compilar seu programa. A seguir está a saída do gcc (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005:

% gcc plusplus.c -Wall -Werror -pedantic
plusplus.c: In function main’:
plusplus.c:6:6: error: operation on i may be undefined [-Werror=sequence-point]
    i = i++ + ++i;
    ~~^~~~~~~~~~~
plusplus.c:6:6: error: operation on i may be undefined [-Werror=sequence-point]
plusplus.c:10:6: error: operation on i may be undefined [-Werror=sequence-point]
    i = (i++);
    ~~^~~~~~~
plusplus.c:14:6: error: operation on u may be undefined [-Werror=sequence-point]
    u = u++ + ++u;
    ~~^~~~~~~~~~~
plusplus.c:14:6: error: operation on u may be undefined [-Werror=sequence-point]
plusplus.c:18:6: error: operation on u may be undefined [-Werror=sequence-point]
    u = (u++);
    ~~^~~~~~~
plusplus.c:22:6: error: operation on v may be undefined [-Werror=sequence-point]
    v = v++ + ++v;
    ~~^~~~~~~~~~~
plusplus.c:22:6: error: operation on v may be undefined [-Werror=sequence-point]
cc1: all warnings being treated as errors

A parte importante é saber o que é um ponto de sequência - e o que é um ponto de sequência e o que não é . Por exemplo, o operador de vírgula é um ponto de sequência, portanto

j = (i ++, ++ i);

é bem definido e será incrementado iem um, produzindo o valor antigo, descarte esse valor; depois, no operador de vírgula, decida os efeitos colaterais; e então incrementa iem um, e o valor resultante se torna o valor da expressão - isto é, apenas uma maneira artificial de escrever, j = (i += 2)que é novamente uma maneira "inteligente" de escrever

i += 2;
j = i;

No entanto, as ,listas de argumentos da função in não são um operador de vírgula e não há ponto de sequência entre avaliações de argumentos distintos; em vez disso, suas avaliações não são sequenciais uma em relação à outra; então a chamada de função

int i = 0;
printf("%d %d\n", i++, ++i, i);

possui comportamento indefinido porque não há ponto de sequência entre as avaliações dos argumentos i++e ++inos argumentos das funções , e o valor de ié, portanto, modificado duas vezes, por ambos i++e ++ientre o ponto de sequência anterior e o próximo.

Antti Haapala
fonte
14

O padrão C diz que uma variável só deve ser atribuída no máximo uma vez entre dois pontos de sequência. Um ponto e vírgula, por exemplo, é um ponto de sequência.
Portanto, toda declaração do formulário:

i = i++;
i = i++ + ++i;

e assim por diante violam essa regra. O padrão também diz que o comportamento é indefinido e não especificado. Alguns compiladores os detectam e produzem algum resultado, mas isso não é por padrão.

No entanto, duas variáveis ​​diferentes podem ser incrementadas entre dois pontos de sequência.

while(*src++ = *dst++);

A descrição acima é uma prática comum de codificação ao copiar / analisar seqüências de caracteres.

Nikhil Vidhani
fonte
Obviamente, isso não se aplica a diferentes variáveis ​​em uma expressão. Seria uma falha total no design se isso acontecesse! Tudo o que você precisa no segundo exemplo é que ambos sejam incrementados entre a instrução que termina e o próximo que começa, e isso é garantido, precisamente devido ao conceito de pontos de sequência no centro de tudo isso.
underscore_d
11

Em /programming/29505280/incrementing-array-index-in-c alguém perguntou sobre uma declaração como:

int k[] = {0,1,2,3,4,5,6,7,8,9,10};
int i = 0;
int num;
num = k[++i+k[++i]] + k[++i];
printf("%d", num);

que imprime 7 ... o OP esperava imprimir 6.

Não ++ié garantido que todos os incrementos sejam concluídos antes do restante dos cálculos. De fato, diferentes compiladores obterão resultados diferentes aqui. No exemplo que você forneceu, os dois primeiros ++iexecutados, os valores de k[]foram lidos e os últimos ++ientão k[].

num = k[i+1]+k[i+2] + k[i+3];
i += 3

Compiladores modernos otimizarão isso muito bem. De fato, possivelmente melhor do que o código que você escreveu originalmente (assumindo que funcionou da maneira que você esperava).

TomOnTime
fonte
5

Uma boa explicação sobre o que acontece neste tipo de computação é fornecido no documento n1188 do site da ISO W14 .

Eu explico as idéias.

A regra principal da norma ISO 9899 que se aplica nessa situação é 6.5p2.

Entre o ponto de sequência anterior e o próximo, um objeto deve ter seu valor armazenado modificado no máximo uma vez pela avaliação de uma expressão. Além disso, o valor anterior deve ser lido apenas para determinar o valor a ser armazenado.

Os pontos de sequência em uma expressão como i=i++são antes i=e depois i++.

No artigo que citei acima, é explicado que você pode descobrir o programa como sendo formado por pequenas caixas, cada uma contendo as instruções entre 2 pontos de sequência consecutivos. Os pontos de sequência são definidos no anexo C da norma, no caso de i=i++existirem 2 pontos de sequência que delimitam uma expressão completa. Tal expressão é sintaticamente equivalente a uma entrada expression-statementna forma Backus-Naur da gramática (uma gramática é fornecida no anexo A da Norma).

Portanto, a ordem das instruções dentro de uma caixa não tem uma ordem clara.

i=i++

pode ser interpretado como

tmp = i
i=i+1
i = tmp

ou como

tmp = i
i = tmp
i=i+1

porque todas essas formas de interpretar o código i=i++são válidas e porque ambas geram respostas diferentes, o comportamento é indefinido.

Portanto, um ponto de sequência pode ser visto no início e no final de cada caixa que compõe o programa [as caixas são unidades atômicas em C] e, dentro de uma caixa, a ordem das instruções não é definida em todos os casos. Mudando essa ordem, pode-se alterar o resultado algumas vezes.

EDITAR:

Outra boa fonte para explicar essas ambiguidades são as entradas do site c-faq (também publicadas como livro ), aqui e aqui e aqui .

alinsoar
fonte
Como esta resposta adicionou novas às respostas existentes? Além disso, as explicações para i=i++são muito semelhantes a esta resposta .
haccks
@haccks Eu não li as outras respostas. Eu queria explicar em meu próprio idioma o que aprendi com o documento mencionado no site oficial da ISO 9899 open-std.org/jtc1/sc22/wg14/www/docs/n1188.pdf
alinsoar
5

Sua pergunta provavelmente não foi "Por que essas construções são um comportamento indefinido em C?". Sua pergunta foi provavelmente: "Por que esse código (usando ++) não me deu o valor que eu esperava?", E alguém marcou sua pergunta como duplicada e o enviou aqui.

Esta resposta tenta responder a essa pergunta: por que seu código não deu a resposta que você esperava e como você pode aprender a reconhecer (e evitar) expressões que não funcionarão conforme o esperado.

Suponho que você tenha ouvido a definição básica de C ++e-- operadores e como o formulário de prefixo ++xdifere do formulário de postfix x++. Mas é difícil pensar nesses operadores; portanto, para ter certeza de que você entendeu, talvez você tenha escrito um pequeno programa de teste envolvendo algo como

int x = 5;
printf("%d %d %d\n", x, ++x, x++);

Mas, para sua surpresa, este programa não o ajudou a entender - ele imprimiu uma saída estranha, inesperada e inexplicável, sugerindo que talvez ++faça algo completamente diferente, nem um pouco o que você pensou que ele fez.

Ou talvez você esteja vendo uma expressão difícil de entender como

int x = 5;
x = x++ + ++x;
printf("%d\n", x);

Talvez alguém tenha lhe dado esse código como um quebra-cabeça. Esse código também não faz sentido, especialmente se você o executa - e se você o compilar e executar em dois compiladores diferentes, é provável que você obtenha duas respostas diferentes! O que há com isso? Qual resposta está correta? (E a resposta é que ambos são, ou nenhum deles é.)

Como você já ouviu falar agora, todas essas expressões são indefinidas , o que significa que a linguagem C não garante o que fará. Esse é um resultado estranho e surpreendente, porque você provavelmente pensou que qualquer programa que pudesse escrever, contanto que fosse compilado e executado, geraria uma saída única e bem definida. Mas no caso de comportamento indefinido, não é assim.

O que torna uma expressão indefinida? As expressões que envolvem ++e-- sempre indefinidas? Claro que não: esses são operadores úteis e, se você os usar corretamente, serão perfeitamente bem definidos.

Para as expressões em que estamos falando, o que as torna indefinidas é quando há muita coisa acontecendo ao mesmo tempo, quando não sabemos ao certo em que ordem as coisas acontecerão, mas quando a ordem importa para o resultado que obtemos.

Vamos voltar aos dois exemplos que usei nesta resposta. Quando eu escrevi

printf("%d %d %d\n", x, ++x, x++);

a questão é, antes de chamar printf, o compilador calcula o valor de xfirst, ou x++, ou talvez ++x? Mas acontece que não sabemos . Não existe uma regra em C que diga que os argumentos para uma função sejam avaliados da esquerda para a direita ou da direita para a esquerda ou em alguma outra ordem. Portanto, não podemos dizer se o compilador vai fazer xem primeiro lugar, então ++x, em seguida x++, ou x++então ++x, em seguida x, ou algum outro fim. Mas a ordem é claramente importante, porque, dependendo de qual ordem o compilador usa, obteremos claramente diferentes resultados impressos printf.

E essa expressão maluca?

x = x++ + ++x;

O problema com esta expressão é que ela contém três tentativas diferentes para modificar o valor de x: (1) a x++parte tenta adicionar 1 a x, armazenar o novo valor em xe retornar o valor antigo de x; (2) a ++xpeça tenta adicionar 1 a x, armazenar o novo valor em xe retornar o novo valor de x; e (3) a x =parte tenta atribuir a soma dos outros dois de volta a x. Qual dessas três tentativas de atribuição "vencerá"? A qual dos três valores será atribuído x? Novamente, e talvez surpreendentemente, não há regra em C para nos dizer.

Você pode imaginar que a precedência ou associatividade ou a avaliação da esquerda para a direita informa em que ordem as coisas acontecem, mas elas não acontecem. Você pode não acreditar em mim, mas aceite minha palavra e digo novamente: precedência e associatividade não determinam todos os aspectos da ordem de avaliação de uma expressão em C. Em particular, se em uma expressão houver várias pontos diferentes em que tentamos atribuir um novo valor a algo como xprecedência e associatividade não nos dizem qual dessas tentativas acontece primeiro, ou último, ou algo assim.


Portanto, com todo esse histórico e introdução fora do caminho, se você quiser garantir que todos os seus programas estejam bem definidos, quais expressões você pode escrever e quais você não pode escrever?

Essas expressões são boas:

y = x++;
z = x++ + y++;
x = x + 1;
x = a[i++];
x = a[i++] + b[j++];
x[i++] = a[j++] + b[k++];
x = *p++;
x = *p++ + *q++;

Essas expressões são todas indefinidas:

x = x++;
x = x++ + ++x;
y = x + x++;
a[i] = i++;
a[i++] = i;
printf("%d %d %d\n", x, ++x, x++);

E a última pergunta é: como você pode dizer quais expressões estão bem definidas e quais são indefinidas?

Como eu disse anteriormente, as expressões indefinidas são aquelas em que há muita coisa ao mesmo tempo, onde você não pode ter certeza de qual ordem as coisas acontecem e onde a ordem importa:

  1. Se houver uma variável que está sendo modificada (atribuída a) em dois ou mais locais diferentes, como você sabe qual modificação acontece primeiro?
  2. Se houver uma variável que está sendo modificada em um local e tendo seu valor usado em outro local, como você sabe se ela usa o valor antigo ou o novo valor?

Como um exemplo de # 1, na expressão

x = x++ + ++x;

existem três tentativas para modificar o `x.

Como um exemplo de # 2, na expressão

y = x + x++;

nós dois usamos o valor de xe o modificamos.

Portanto, essa é a resposta: verifique se, em qualquer expressão que você escreve, cada variável é modificada no máximo uma vez e, se uma variável é modificada, você também não tenta usar o valor dessa variável em outro lugar.

Steve Summit
fonte
3

O motivo é que o programa está executando um comportamento indefinido. O problema está na ordem de avaliação, porque não há pontos de sequência necessários de acordo com o padrão C ++ 98 (nenhuma operação é sequenciada antes ou depois do outro, de acordo com a terminologia do C ++ 11).

No entanto, se você se ater a um compilador, encontrará o comportamento persistente, desde que não adicione chamadas de função ou ponteiros, o que tornaria o comportamento mais confuso.

  • Então, primeiro o GCC: Usando o Nuwen MinGW 15 GCC 7.1, você obterá:

    #include<stdio.h>
    int main(int argc, char ** argv)
    {
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 2
    
    i = 1;
    i = (i++);
    printf("%d\n", i); //1
    
    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 2
    
    u = 1;
    u = (u++);
    printf("%d\n", u); //1
    
    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); //2

    }

Como o GCC funciona? avalia subexpressões da esquerda para a direita do lado direito (RHS) e atribui o valor ao lado esquerdo (LHS). É exatamente assim que Java e C # se comportam e definem seus padrões. (Sim, o software equivalente em Java e C # definiu comportamentos). Ele avalia cada sub-expressão, uma por uma na Declaração RHS, na ordem da esquerda para a direita; para cada subexpressão: o ++ c (pré-incremento) é avaliado primeiro e depois o valor c é usado para a operação, depois o pós-incremento c ++).

de acordo com o GCC C ++: operadores

No GCC C ++, a precedência dos operadores controla a ordem em que os operadores individuais são avaliados

o código equivalente no comportamento definido C ++, conforme o GCC entende:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    //i = i++ + ++i;
    int r;
    r=i;
    i++;
    ++i;
    r+=i;
    i=r;
    printf("%d\n", i); // 2

    i = 1;
    //i = (i++);
    r=i;
    i++;
    i=r;
    printf("%d\n", i); // 1

    volatile int u = 0;
    //u = u++ + ++u;
    r=u;
    u++;
    ++u;
    r+=u;
    u=r;
    printf("%d\n", u); // 2

    u = 1;
    //u = (u++);
    r=u;
    u++;
    u=r;
    printf("%d\n", u); // 1

    register int v = 0;
    //v = v++ + ++v;
    r=v;
    v++;
    ++v;
    r+=v;
    v=r;
    printf("%d\n", v); //2
}

Então vamos ao Visual Studio . Visual Studio 2015, você obtém:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 3

    i = 1;
    i = (i++);
    printf("%d\n", i); // 2 

    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 3

    u = 1;
    u = (u++);
    printf("%d\n", u); // 2 

    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); // 3 
}

Como o visual studio funciona, adota outra abordagem, avalia todas as expressões de pré-incrementos na primeira passagem, usa valores de variáveis ​​nas operações na segunda passagem, atribui de RHS a LHS na terceira passagem e, na última passagem, avalia todas as expressões pós-incremento em uma passagem.

Portanto, o equivalente no comportamento definido C ++ como o Visual C ++ entende:

#include<stdio.h>
int main(int argc, char ** argv)
{
    int r;
    int i = 0;
    //i = i++ + ++i;
    ++i;
    r = i + i;
    i = r;
    i++;
    printf("%d\n", i); // 3

    i = 1;
    //i = (i++);
    r = i;
    i = r;
    i++;
    printf("%d\n", i); // 2 

    volatile int u = 0;
    //u = u++ + ++u;
    ++u;
    r = u + u;
    u = r;
    u++;
    printf("%d\n", u); // 3

    u = 1;
    //u = (u++);
    r = u;
    u = r;
    u++;
    printf("%d\n", u); // 2 

    register int v = 0;
    //v = v++ + ++v;
    ++v;
    r = v + v;
    v = r;
    v++;
    printf("%d\n", v); // 3 
}

como a documentação do Visual Studio declara em Precedência e ordem de avaliação :

Onde vários operadores aparecem juntos, eles têm a mesma precedência e são avaliados de acordo com sua associatividade. Os operadores na tabela são descritos nas seções que começam com Operadores Postfix.

Mohamed El-Nakib
fonte
1
Eu editei a pergunta para adicionar o UB na avaliação dos argumentos das funções, pois essa pergunta é frequentemente usada como duplicata para isso. (O último exemplo)
Antti Haapala 21/10
1
Além disso, a pergunta é sobre c agora, não C ++
Antti Haapala