Comportamento indefinido e pontos de sequência

987

O que são "pontos de sequência"?

Qual é a relação entre comportamento indefinido e pontos de sequência?

Costumo usar expressões engraçadas e complicadas como a[++i] = i;, para me sentir melhor. Por que devo parar de usá-los?

Se você leu isso, visite a pergunta de acompanhamento Comportamento indefinido e os pontos de sequência recarregados .

(Observação: isso deve ser uma entrada para as Perguntas frequentes sobre C ++ do Stack Overflow . Se você quiser criticar a idéia de fornecer uma FAQ neste formulário, a postagem na meta que iniciou tudo isso seria o lugar para isso. essa pergunta é monitorada na sala de chat do C ++ , onde a idéia de FAQ começou em primeiro lugar; portanto, é muito provável que sua resposta seja lida pelos que a tiveram.)

desconhecidos
fonte

Respostas:

683

C ++ 98 e C ++ 03

Esta resposta é para as versões mais antigas do padrão C ++. As versões C ++ 11 e C ++ 14 do padrão não contêm formalmente 'pontos de sequência'; operações são 'sequenciadas antes' ou 'não sequenciadas' ou 'indeterminadamente sequenciadas'. O efeito líquido é essencialmente o mesmo, mas a terminologia é diferente.


Disclaimer : Ok. Essa resposta é um pouco longa. Portanto, tenha paciência ao lê-lo. Se você já sabe essas coisas, lê-las novamente não o deixará louco.

Pré-requisitos : Um conhecimento elementar do Padrão C ++


O que são pontos de sequência?

O Padrão diz

Em certos pontos especificados na sequência de execução chamados pontos de sequência , todos os efeitos colaterais das avaliações anteriores devem estar completos e nenhum efeito colateral das avaliações subsequentes deve ter ocorrido. (§1.9 / 7)

Efeitos colaterais? Quais são os efeitos colaterais?

A avaliação de uma expressão produz algo e se, além disso, há uma alteração no estado do ambiente de execução, diz-se que a expressão (sua avaliação) tem alguns efeitos colaterais.

Por exemplo:

int x = y++; //where y is also an int

Além da operação de inicialização, o valor de yé alterado devido ao efeito colateral do ++operador.

Por enquanto, tudo bem. Passando para os pontos de sequência. Uma definição de alternância de pontos seq fornecida pelo autor comp.lang.c Steve Summit:

O ponto de sequência é um momento no qual a poeira se depositou e todos os efeitos colaterais vistos até agora são garantidos.


Quais são os pontos de sequência comuns listados no padrão C ++?

Esses são:

  • no final da avaliação da expressão completa ( §1.9/16) (Uma expressão completa é uma expressão que não é uma subexpressão de outra expressão.) 1

    Exemplo:

    int a = 5; // ; is a sequence point here
  • na avaliação de cada uma das seguintes expressões após a avaliação da primeira expressão ( §1.9/18) 2

    • a && b (§5.14)
    • a || b (§5.15)
    • a ? b : c (§5.16)
    • a , b (§5.18)(aqui a, b é um operador de vírgula; in func(a,a++) ,não é um operador de vírgula, é apenas um separador entre os argumentos ae a++. Portanto, o comportamento é indefinido nesse caso (se afor considerado um tipo primitivo))
  • em uma chamada de função (esteja a função em linha ou não), após a avaliação de todos os argumentos da função (se houver) que ocorrem antes da execução de quaisquer expressões ou instruções no corpo da função ( §1.9/17).

1: Nota: a avaliação de uma expressão completa pode incluir a avaliação de subexpressões que não fazem parte lexicamente da expressão completa. Por exemplo, subexpressões envolvidas na avaliação de expressões de argumento padrão (8.3.6) são consideradas criadas na expressão que chama a função, não na expressão que define o argumento padrão

2: Os operadores indicados 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 assim 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.


O que é comportamento indefinido?

O Padrão define Comportamento Indefinido na Seção §1.3.12como

comportamento, como pode surgir com o uso de uma construção de programa ou dados errados, para os quais esta Norma Internacional não impõe requisitos 3 .

Um comportamento indefinido também pode ser esperado quando esta Norma Internacional omite a descrição de qualquer definição explícita de comportamento.

3: o comportamento indefinido permitido varia de ignorar completamente a situação com resultados imprevisíveis, comportar-se 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).

Em resumo, um comportamento indefinido significa que qualquer coisa pode acontecer, desde daemons voando pelo nariz até a namorada engravidar.


Qual é a relação entre comportamento indefinido e pontos de sequência?

Antes de entrar nesse assunto, você deve conhecer as diferenças entre comportamento indefinido, comportamento não especificado e comportamento definido para implementação .

Você também deve saber disso the order of evaluation of operands of individual operators and subexpressions of individual expressions, and the order in which side effects take place, is unspecified.

Por exemplo:

int x = 5, y = 6;

int z = x++ + y++; //it is unspecified whether x++ or y++ will be evaluated first.

Outro exemplo aqui .


Agora, o padrão §5/4diz

  • 1) Entre o ponto de sequência anterior e o próximo, um objeto escalar deve ter seu valor armazenado modificado no máximo uma vez pela avaliação de uma expressão.

O que isso significa?

Informalmente, significa que entre dois pontos de sequência uma variável não deve ser modificada mais de uma vez. Em uma declaração de expressão, next sequence pointgeralmente está no ponto-e-vírgula final e previous sequence pointno final da declaração anterior. Uma expressão também pode conter intermediários sequence points.

Na sentença acima, as seguintes expressões invocam o comportamento indefinido:

i++ * ++i;   // UB, i is modified more than once btw two SPs
i = ++i;     // UB, same as above
++i = 2;     // UB, same as above
i = ++i + 1; // UB, same as above
++++++i;     // UB, parsed as (++(++(++i)))

i = (i, ++i, ++i); // UB, there's no SP between `++i` (right most) and assignment to `i` (`i` is modified more than once btw two SPs)

Mas as seguintes expressões são boas:

i = (i, ++i, 1) + 1; // well defined (AFAIK)
i = (++i, i++, i);   // well defined 
int j = i;
j = (++i, i++, j*i); // well defined

  • 2) Além disso, o valor anterior deve ser acessado apenas para determinar o valor a ser armazenado.

O que isso significa? Isso significa que, se um objeto é gravado em uma expressão completa, todo e qualquer acesso a ele dentro da mesma expressão deve estar diretamente envolvido no cálculo do valor a ser gravado .

Por exemplo, em i = i + 1todo o acesso de i(no LHS e no RHS) estão diretamente envolvidos no cálculo do valor a ser gravado. Então está tudo bem.

Essa regra restringe efetivamente expressões legais àquelas em que os acessos precedem comprovadamente a modificação.

Exemplo 1:

std::printf("%d %d", i,++i); // invokes Undefined Behaviour because of Rule no 2

Exemplo 2:

a[i] = i++ // or a[++i] = i or a[i++] = ++i etc

é proibido porque um dos acessos de i(aquele em a[i]) não tem nada a ver com o valor que acaba sendo armazenado em i (o que acontece em i++), e, portanto, não há uma boa maneira de definir - tanto para o nosso entendimento quanto para o compilador - se o acesso deve ocorrer antes ou depois do valor incrementado ser armazenado. Portanto, o comportamento é indefinido.

Exemplo 3:

int x = i + i++ ;// Similar to above

Resposta de acompanhamento para C ++ 11 aqui .

Prasoon Saurav
fonte
45
*p++ = 4 não é um comportamento indefinido. *p++é interpretado como *(p++). p++retorna p(uma cópia) e o valor armazenado no endereço anterior. Por que isso invocaria o UB? Está perfeitamente bem.
Prasoon Saurav
7
@ Mike: AFAIK, não há cópias (legais) do C ++ Standard às quais você poderia vincular.
SBI
11
Bem, então você pode ter um link para a página de pedidos relevante da ISO. Enfim, pensando bem, a frase "conhecimento elementar do padrão C ++" parece uma contradição em termos, pois se você estiver lendo o padrão, estará além do nível elementar. Talvez possamos listar de que coisas na linguagem você precisa de um entendimento básico, como sintaxe da expressão, ordem das operações e talvez sobrecarga do operador?
Mike DeSimone
41
Não tenho certeza de que citar o padrão é a melhor maneira de ensinar iniciantes
Inverse
6
@Adrian A primeira expressão chama um UB porque não há um ponto de sequência entre o último ++ie a atribuição a i. A segunda expressão não chama UB porque a expressão inão altera o valor de i. No segundo exemplo, i++é seguido por um ponto de sequência ( ,) antes que o operador de atribuição seja chamado.
Kolyunya
276

Este é um acompanhamento da minha resposta anterior e contém material relacionado ao C ++ 11. .


Pré-requisitos : Um conhecimento elementar de Relações (Matemática).


É verdade que não há pontos de sequência no C ++ 11?

Sim! Isso é verdade.

Os pontos de sequência foram substituídos pelas relações Sequenced Before e Sequenced After (e Unsequenced e Indeterminately Sequenced ) no C ++ 11.


O que exatamente é essa coisa de 'Sequenciado antes'?

Sequenced Before (§1.9 / 13) é uma relação que é:

entre avaliações executadas por um único encadeamento e induz uma ordem parcial estrita 1

Formalmente significa que são dadas duas avaliações (veja abaixo) A e B, se Ajá tiver sido sequenciada antes B , a execução de A precederá a execução de B. Se Anão é seqüenciado antes Be Bnão é seqüenciado antes A, então Ae Bsão unsequenced 2 .

Avaliações Ae Bsão sequenciadas indeterminadamente quando uma Aé sequenciada antes Bou Bé sequenciada antes A, mas não é especificado qual 3 .

[Notas]
1: Uma ordem estrita parcial é uma relação binária "<" ao longo de um conjunto Pque é asymmetric, e transitive, ou seja, para todos a, be cem P, temos que:
........ (i). se a <b então ¬ (b <a) ( asymmetry);
........ ii) se a <b e b <c então a <c ( transitivity).
2: A execução de avaliações não subsequentes pode se sobrepor .
3: Avaliações seqüenciadas indeterminadamente não podem se sobrepor , mas podem ser executadas primeiro.


Qual é o significado da palavra 'avaliação' no contexto do C ++ 11?

No C ++ 11, a avaliação de uma expressão (ou subexpressão) em geral inclui:

  • cálculos de valores (incluindo a determinação da identidade de um objeto para avaliação de valor de valor e a busca de um valor previamente atribuído a um objeto para avaliação de valor de valor ) e

  • início de efeitos colaterais .

Agora (§1.9 / 14) diz:

Todo cálculo de valor e efeito colateral associado a uma expressão completa é sequenciado antes de todo cálculo de valor e efeito colateral associado à próxima expressão completa a ser avaliada .

  • Exemplo trivial:

    int x; x = 10; ++x;

    O cálculo do valor e o efeito colateral associado a ele ++xsão sequenciados após o cálculo do valor e o efeito colateral dex = 10;


Portanto, deve haver alguma relação entre o comportamento indefinido e as coisas acima mencionadas, certo?

Sim! Direita.

Em (§1.9 / 15), foi mencionado que

Excepto quando mencionado, as avaliações de operandos de operadores individuais e de subexpress~oes de expressões individuais são unsequenced 4 .

Por exemplo :

int main()
{
     int num = 19 ;
     num = (num << 3) + (num >> 3);
} 
  1. A avaliação dos operandos do +operador não é sequencial entre si.
  2. A avaliação de operandos <<e >>operadores não é sequencial entre si.

4: Em uma expressão que é avaliada mais de uma vez durante a execução de um programa, unsequenced e indeterminadamente seqüenciado avaliações das suas subexpressões não precisam ser executadas de forma consistente em diferentes avaliações.

(§1.9 / 15) Os cálculos dos valores dos operandos de um operador são sequenciados antes do cálculo dos valores do resultado do operador.

Isso significa no x + ycálculo do valor de xe yé sequenciado antes do cálculo do valor de (x + y).

Mais importante

(§1.9 / 15) Se um efeito colateral em um objeto escalar não for conseqüente em relação a

(a) outro efeito colateral no mesmo objeto escalar

ou

(b) uma computação de valor usando o valor do mesmo objeto escalar.

o comportamento é indefinido .

Exemplos:

int i = 5, v[10] = { };
void  f(int,  int);
  1. i = i++ * ++i; // Undefined Behaviour
  2. i = ++i + i++; // Undefined Behaviour
  3. i = ++i + ++i; // Undefined Behaviour
  4. i = v[i++]; // Undefined Behaviour
  5. i = v[++i]: // Well-defined Behavior
  6. i = i++ + 1; // Undefined Behaviour
  7. i = ++i + 1; // Well-defined Behaviour
  8. ++++i; // Well-defined Behaviour
  9. f(i = -1, i = -1); // Undefined Behaviour (see below)

Ao chamar uma função (esteja a função em linha ou não), todo cálculo de valor e efeito colateral associado a qualquer expressão de argumento, ou à expressão postfix que designa a função chamada, são sequenciados antes da execução de cada expressão ou instrução no corpo do chamada função. [ Nota: cálculos de valor e efeitos colaterais associados a diferentes expressões de argumento não têm seqüência . - nota final ]

Expressões (5), (7)e (8)não invocar um comportamento indefinido. Confira as seguintes respostas para uma explicação mais detalhada.


Nota final :

Se você encontrar alguma falha no post, deixe um comentário. Usuários avançados (Com rep> 20000), não hesite em editar a postagem para corrigir erros de digitação e outros erros.

Prasoon Saurav
fonte
3
Em vez de "assimétrico", sequenciadas antes / depois são relações "antissimétricas". Isso deve ser alterado no texto para se adaptar à definição de uma ordem parcial dada posteriormente (que também concorda com a Wikipedia).
TemplateRex
1
Por que o item 7) no último exemplo é um UB? Talvez devesse ser f(i = -1, i = 1)?
22414 Mikhail
1
Corrigi a descrição da relação "sequenciada antes". É uma ordem parcial estrita . Obviamente, uma expressão não pode ser sequenciada antes de si mesma, portanto a relação não pode ser reflexiva. Portanto, é assimétrico, não anti-simétrico.
ThomasMcLeod
1
5) estar bem confinado me deixou louco. a explicação de Johannes Schaub não era totalmente fácil de entender. Especialmente porque eu acreditava que, mesmo em ++i(sendo o valor avaliado antes do +operador que o está usando), o padrão ainda não diz que seu efeito colateral deve ser concluído. Mas, de fato, porque retorna um ref para um lvalueque é iele mesmo, DEVE ter finalizado o efeito colateral, pois a avaliação deve ser concluída, portanto, o valor deve estar atualizado. Essa era a parte louca para se entender.
precisa saber é
"Os membros do Comitê ISO C ++ consideraram que os itens dos Sequence Points eram bastante difíceis de entender. Então, eles decidiram substituí-lo pelas relações acima mencionadas apenas para uma redação mais clara e maior precisão". - você tem uma referência para essa reivindicação? Parece-me que as novas relações são mais difíceis de entender.
MM
30

C ++ 17 (N4659 ) inclui uma proposta Refining Order de Avaliação de Expressão para Idiomatic C ++, que define uma ordem mais estrita de avaliação de expressão.

Em particular, a seguinte frase

8.18 Operadores de atribuição e atribuição composta :
....

Em todos os casos, a atribuição é sequenciada após o cálculo do valor dos operandos direito e esquerdo e antes do cálculo do valor da expressão de atribuição. O operando direito é sequenciado antes do operando esquerdo.

juntamente com os seguintes esclarecimentos

Diz-se que uma expressão X é sequenciada antes de uma expressão Y se todo cálculo de valor e todo efeito colateral associado à expressão X for sequenciado antes de todo cálculo de valor e todo efeito colateral associado à expressão Y .

validar vários casos de comportamento anteriormente indefinido, incluindo o em questão:

a[++i] = i;

No entanto, vários outros casos semelhantes ainda levam a um comportamento indefinido.

Em N4140:

i = i++ + 1; // the behavior is undefined

Mas em N4659

i = i++ + 1; // the value of i is incremented
i = i++ + i; // the behavior is undefined

Obviamente, o uso de um compilador compatível com C ++ 17 não significa necessariamente que se deva começar a escrever essas expressões.

AlexD
fonte
por que o i = i++ + 1;comportamento é definido no c ++ 17, acho que mesmo que "O operando direito seja sequenciado antes do operando esquerdo", no entanto, a modificação para "i ++" e o efeito colateral da atribuição não são subsequentes, por favor, forneça mais detalhes para interpretá-los
jack X
@jackX Estendi a resposta :).
AlexD 22/03
Sim, acho que o detalhe da interpretação da frase "O operando direito é sequenciado antes do operando esquerdo" é mais útil. Como "O operando direito é sequenciado antes do operando esquerdo" significa o cálculo do valor e o efeito colateral associado ao operando direito. sequenciado antes do operando esquerdo. como você fez :-)
jack X
11

Acho que há uma razão fundamental para a mudança, não é apenas cosmético tornar a interpretação antiga mais clara: essa razão é simultaneidade. A ordem não especificada de elaboração é apenas a seleção de uma das várias ordens em série possíveis, isso é bem diferente das ordens antes e depois, porque, se não houver ordem especificada, é possível uma avaliação simultânea: não com as regras antigas. Por exemplo em:

f (a,b)

anteriormente a então b ou b então a. Agora, aeb podem ser avaliados com instruções intercaladas ou mesmo em núcleos diferentes.

Yttrill
fonte
5
Acredito, porém, que, se 'a' ou 'b' inclui uma chamada de função, eles são sequenciados indeterminadamente, e não sequenciados, ou seja, todos os efeitos colaterais de um devem ocorrer antes de qualquer efeito colateral do outro, embora o compilador não precise ser consistente sobre qual deles será o primeiro. Se isso não for mais verdade, ele quebraria muito código, que depende de operações que não se sobrepõem (por exemplo, se 'a' e 'b' configuram, usam e desativam um estado estático compartilhado).
Supercat 9/12
2

No C99(ISO/IEC 9899:TC3)qual parece ausente desta discussão até o momento, os seguintes steteents são feitos em relação à ordem de avaliação.

[...] a ordem de avaliação das subexpressões e a ordem na qual os efeitos colaterais ocorrem não são especificadas. (Seção 6.5 pp 67)

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 [sic] será indefinido (Seção 6.5.16, p. 91)

awiebe
fonte
2
A pergunta está marcada como C ++ e não C, o que é bom porque o comportamento no C ++ 17 é bem diferente do comportamento nas versões mais antigas - e não tem relação com o comportamento em C11, C99, C90, etc. Ou tem muito pouco relação a ele. No geral, eu sugiro remover isso. Mais significativamente, precisamos encontrar as perguntas e respostas equivalentes para C e garantir que esteja tudo bem (e observa que o C ++ 17, em particular, altera as regras - o comportamento no C ++ 11 e antes era mais ou menos o mesmo que em C11, embora a verborragia que a descreve em C ainda use 'pontos de sequência', enquanto o C ++ 11 e posterior não. #
Jonathan Leffler