Os ramos com comportamento indefinido podem ser considerados inalcançáveis ​​e otimizados como código morto?

88

Considere a seguinte declaração:

*((char*)NULL) = 0; //undefined behavior

Evoca claramente um comportamento indefinido. A existência de tal instrução em um determinado programa significa que todo o programa é indefinido ou que o comportamento só se torna indefinido quando o fluxo de controle atinge essa instrução?

O programa a seguir estaria bem definido caso o usuário nunca inserisse o número 3?

while (true) {
 int num = ReadNumberFromConsole();
 if (num == 3)
  *((char*)NULL) = 0; //undefined behavior
}

Ou é um comportamento totalmente indefinido, não importa o que o usuário insira?

Além disso, o compilador pode assumir que o comportamento indefinido nunca será executado em tempo de execução? Isso permitiria raciocinar para trás no tempo:

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

Aqui, o compilador pode raciocinar que, no caso num == 3, sempre invocaremos um comportamento indefinido. Portanto, este caso deve ser impossível e o número não precisa ser impresso. Toda a ifdeclaração pode ser otimizada. Esse tipo de raciocínio reverso é permitido de acordo com o padrão?

usr
fonte
19
às vezes me pergunto se usuários com muitos representantes obtêm mais votos positivos nas perguntas porque "oh, eles têm muitos representantes, essa deve ser uma boa pergunta" ... mas neste caso eu li a pergunta e pensei "uau, isso é ótimo "antes mesmo de olhar para o autor da pergunta.
turbulencetoo
4
Eu acho que o momento em que o comportamento indefinido emerge, é indefinido.
eerorika de
6
O padrão C ++ diz explicitamente que um caminho de execução com comportamento indefinido em qualquer ponto é completamente indefinido. Eu até interpretaria como dizendo que qualquer programa com comportamento indefinido no caminho é completamente indefinido (isso inclui resultados razoáveis ​​em outras partes, mas não é garantido). Os compiladores podem usar o comportamento indefinido para modificar seu programa. blog.llvm.org/2011/05/what-every-c-programmer-should-know.html contém alguns bons exemplos.
Jens de
4
@Jens: Na verdade, significa apenas o caminho de execução. Caso contrário, você terá problemas const int i = 0; if (i) 5/i;.
MSalters
1
O compilador em geral não pode provar que PrintToConsolenão chama, std::exitentão ele tem que fazer a chamada.
MSalters de

Respostas:

65

A existência de tal instrução em um determinado programa significa que todo o programa é indefinido ou que o comportamento só se torna indefinido quando o fluxo de controle atinge essa instrução?

Nem. A primeira condição é muito forte e a segunda é muito fraca.

O acesso a objetos às vezes é sequenciado, mas o padrão descreve o comportamento do programa fora do tempo. Danvil já citado:

se qualquer execução contiver uma operação indefinida, esta Norma Internacional não impõe nenhum requisito sobre a implementação que executa esse programa com essa entrada (nem mesmo no que diz respeito a operações anteriores à primeira operação indefinida)

Isso pode ser interpretado:

Se a execução do programa produzir um comportamento indefinido, então todo o programa terá um comportamento indefinido.

Portanto, uma instrução inacessível com UB não dá ao programa UB. Uma declaração alcançável que (por causa dos valores das entradas) nunca é alcançada, não dá ao programa UB. É por isso que sua primeira condição é muito forte.

Agora, o compilador em geral não pode dizer o que UB tem. Portanto, para permitir que o otimizador reordene instruções com UB potencial que seria reordenado caso seu comportamento fosse definido, é necessário permitir que UB "volte no tempo" e dê errado antes do ponto de sequência anterior (ou em C ++ 11 terminologia, para o UB afetar coisas que são sequenciadas antes da coisa do UB). Portanto, sua segunda condição é muito fraca.

Um exemplo importante disso é quando o otimizador depende de aliasing estrito. O objetivo principal das regras de aliasing estritas é permitir que o compilador reordene operações que não poderiam ser reordenadas validamente se fosse possível que os ponteiros em questão usassem o alias da mesma memória. Portanto, se você usar ponteiros de aliasing ilegalmente, e o UB ocorrer, ele pode facilmente afetar uma instrução "antes" da instrução UB. No que diz respeito à máquina abstrata, a instrução UB ainda não foi executada. No que diz respeito ao código-objeto real, ele foi executado parcial ou totalmente. Mas o padrão não tenta entrar em detalhes sobre o que significa para o otimizador reordenar as instruções, ou quais são as implicações disso para o UB. Ele apenas dá a licença de implementação para que dê errado assim que for conveniente.

Você pode pensar nisso como "UB tem uma máquina do tempo".

Especificamente para responder aos seus exemplos:

  • O comportamento só é indefinido se 3 for lido.
  • Os compiladores podem e eliminam o código como morto se um bloco básico contiver uma operação com certeza indefinida. Eles são permitidos (e estou supondo que sim) em casos que não são um bloco básico, mas onde todos os ramos levam ao UB. Este exemplo não é um candidato a menos que de PrintToConsole(3)alguma forma tenha certeza de que retornará. Isso poderia lançar uma exceção ou algo assim.

Um exemplo semelhante ao seu segundo é a opção gcc -fdelete-null-pointer-checks, que pode receber código como este (não verifiquei este exemplo específico, considere-o ilustrativo da ideia geral):

void foo(int *p) {
    if (p) *p = 3;
    std::cout << *p << '\n';
}

e mude para:

*p = 3;
std::cout << "3\n";

Por quê? Porque if pé nulo, então o código tem UB de qualquer maneira, então o compilador pode assumir que não é nulo e otimizar de acordo. O kernel do linux tropeçou nisso ( https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897 ) essencialmente porque ele opera em um modo em que a desreferenciação de um ponteiro nulo não deveria ser UB, espera-se que resulte em uma exceção de hardware definida que o kernel pode manipular. Quando a otimização está habilitada, o gcc requer o uso de -fno-delete-null-pointer-checkspara fornecer aquela garantia além do padrão.

PS A resposta prática à pergunta "quando ocorre o comportamento indefinido?" é "10 minutos antes de você planejava sair para o dia".

Steve Jessop
fonte
4
Na verdade, ocorreram alguns problemas de segurança devido a isso no passado. Em particular, qualquer verificação de estouro após o fato corre o risco de ser otimizada devido a isso. Por exemplo void can_add(int x) { if (x + 100 < x) complain(); }pode ser otimizado afastado por completo, porque se x+100 doesn' estouro nada acontece, e se x+100 faz transbordamento, que é UB acordo com a norma, então nada pode acontecer.
fgp
3
@fgp: certo, essa é uma otimização da qual as pessoas reclamam amargamente se tropeçarem nela, porque começa a parecer que o compilador está deliberadamente quebrando seu código para puni-lo. "Por que eu teria escrito assim se quisesse que você removesse!" ;-) Mas eu acho que às vezes é útil para o otimizador ao manipular expressões aritméticas maiores, assumir que não há estouro e evitar qualquer coisa cara que seria necessária apenas nesses casos.
Steve Jessop de
2
Seria correto dizer que o programa não é indefinido se o usuário nunca insere 3, mas se ele insere 3 durante uma execução, toda a execução se torna indefinida? Assim que for 100% certo que o programa irá invocar o comportamento indefinido (e não antes disso), o comportamento se torna permitido ser qualquer coisa. Estas minhas afirmações estão 100% corretas?
usr
3
@usr: Acho que está correto, sim. Com o seu exemplo particular (e fazendo algumas suposições sobre a inevitabilidade dos dados sendo processados), acho que uma implementação poderia, em princípio, olhar para frente em STDIN com buffer para um, 3se quisesse, e despachar para casa durante o dia assim que vi um entrada.
Steve Jessop de
3
Um +1 extra (se eu pudesse) para seu PS
Fred Larson
10

O padrão indica 1,9 / 4

[Nota: Este Padrão Internacional não impõe requisitos sobre o comportamento de programas que contêm comportamento indefinido. - nota final]

O ponto interessante é provavelmente o que significa "conter". Um pouco mais tarde, em 1.9 / 5, afirma:

No entanto, se qualquer execução contiver uma operação indefinida, esta Norma Internacional não impõe nenhum requisito sobre a implementação que executa esse programa com essa entrada (nem mesmo no que diz respeito às operações anteriores à primeira operação indefinida)

Aqui, ele menciona especificamente "execução ... com essa entrada". Eu interpretaria isso como um comportamento indefinido em uma ramificação possível que não é executada agora não influencia a ramificação atual de execução.

Um problema diferente, entretanto, são as suposições baseadas em um comportamento indefinido durante a geração do código. Veja a resposta de Steve Jessop para mais detalhes sobre isso.

Danvil
fonte
1
Se tomado literalmente, essa é a sentença de morte para todos os programas reais existentes.
usr de
6
Eu não acho que a questão era se o UB pode aparecer antes que o código seja realmente alcançado. A questão, como eu entendi, era se o UB poderia aparecer se o código nem fosse alcançado. E, claro, a resposta é "não".
sepp2k de
Bem, o padrão não é tão claro sobre isso em 1.9 / 4, mas 1.9 / 5 pode possivelmente ser interpretado como o que você disse.
Danvil de
1
As notas não são normativas. 1.9 / 5 supera a nota em 1.9 / 4
MSalters de
5

Um exemplo instrutivo é

int foo(int x)
{
    int a;
    if (x)
        return a;
    return 0;
}

Tanto o GCC quanto o Clang atuais irão otimizar isso (em x86) para

xorl %eax,%eax
ret

porque eles deduzem que xé sempre zero do UB no if (x)caminho de controle. O GCC nem mesmo dará a você um aviso de uso de valor não inicializado! (porque o passe que aplica a lógica acima é executado antes do passe que gera avisos de valor não inicializado)

zwol
fonte
1
Exemplo interessante. É bastante desagradável que a ativação da otimização esconda o aviso. Isso nem mesmo está documentado - os documentos do GCC apenas dizem que habilitar a otimização produz mais avisos.
sleske
@sleske É desagradável, eu concordo, mas avisos de valor não inicializado são notoriamente difíceis de "acertar" - fazê-los perfeitamente equivale ao Problema de Parada, e os programadores ficam estranhamente irracionais ao adicionar inicializações de variáveis ​​"desnecessárias" para silenciar falsos positivos, então os autores de compiladores acabam perdendo o tempo. Eu costumava hackear o GCC e lembro que todo mundo tinha medo de mexer com o passe de aviso de valor não inicializado.
zwol
@zwol: Eu me pergunto quanto da "otimização" resultante de tal eliminação de código morto realmente torna o código útil menor, e quanto acaba fazendo com que os programadores tornem o código maior (por exemplo, adicionando código para inicializar amesmo se em todas as circunstâncias onde um não inicializado aseria passado para a função que a função nunca faria nada com ele)?
supercat
@supercat Não estive profundamente envolvido no trabalho de compilador em cerca de 10 anos e é quase impossível raciocinar sobre otimizações a partir de exemplos de brinquedos. Esse tipo de otimização tende a ser associado a reduções gerais de tamanho de código de 2 a 5% em aplicativos reais, se bem me lembro.
zwol
1
@supercat 2-5% é enorme para essas coisas. Já vi pessoas suarem 0,1%.
zwol
4

O atual rascunho de trabalho do C ++ diz em 1.9.4 que

Este Padrão Internacional não impõe requisitos sobre o comportamento de programas que contêm comportamento indefinido.

Com base nisso, eu diria que um programa contendo comportamento indefinido em qualquer caminho de execução pode fazer qualquer coisa a cada momento de sua execução.

Existem dois artigos muito bons sobre comportamento indefinido e o que os compiladores costumam fazer:

Jens
fonte
1
Isso não faz sentido. A função int f(int x) { if (x > 0) return 100/x; else return 100; }certamente nunca invoca um comportamento indefinido, embora 100/0seja, obviamente, indefinido.
fgp
1
@fgp O que o padrão (especialmente 1.9 / 5) diz, entretanto, é que se um comportamento indefinido pode ser alcançado, não importa quando ele é alcançado. Por exemplo, printf("Hello, World"); *((char*)NULL) = 0 não há garantia de impressão de nada. Isso ajuda na otimização, porque o compilador pode reordenar livremente as operações (sujeitas a restrições de dependência, é claro) que ele sabe que ocorrerão eventualmente, sem ter que levar em conta o comportamento indefinido.
fgp
Eu diria que um programa com sua função não contém comportamento indefinido, pois não há entrada onde 100/0 será avaliado.
Jens de
1
Exatamente - então o que importa é se o UB pode realmente ser acionado ou não, não se pode teoricamente ser acionado. Ou você está preparado para argumentar que int x,y; std::cin >> x >> y; std::cout << (x+y);é permitido dizer que "1 + 1 = 17", apenas porque há algumas entradas onde x+yestoura (que é UB, pois inté um tipo com sinal).
fgp
Formalmente, eu diria que o programa tem comportamento indefinido porque existem entradas que o acionam. Mas você está certo de que isso não faz sentido no contexto de C ++, porque seria impossível escrever um programa sem um comportamento indefinido. Eu gostaria quando houvesse menos comportamento indefinido em C ++, mas não é assim que a linguagem funciona (e há alguns bons motivos para isso, mas eles não dizem respeito ao meu uso diário ...).
Jens de
3

A palavra "comportamento" significa que algo está sendo feito . Um statemenr que nunca é executado não é "comportamento".

Uma ilustração:

*ptr = 0;

Esse é um comportamento indefinido? Suponha que estejamos 100% certos ptr == nullptrpelo menos uma vez durante a execução do programa. A resposta deve ser sim.

Que tal isso?

 if (ptr) *ptr = 0;

Isso é indefinido? (Lembra-se de ptr == nullptrpelo menos uma vez?) Espero que não, caso contrário, você não conseguirá escrever nenhum programa útil.

Nenhum srandardês foi prejudicado ao formular essa resposta.

n. 'pronomes' m.
fonte
3

O comportamento indefinido ocorre quando o programa irá causar comportamento indefinido, não importa o que aconteça a seguir. No entanto, você deu o seguinte exemplo.

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

A menos que o compilador conheça a definição de PrintToConsole, ele não pode remover if (num == 3)condicional. Vamos supor que você tenha o LongAndCamelCaseStdio.hcabeçalho do sistema com a seguinte declaração de PrintToConsole.

void PrintToConsole(int);

Nada muito útil, certo. Agora, vamos ver o quão mau (ou talvez não tão mau, o comportamento indefinido poderia ser pior) o fornecedor é, verificando a definição real desta função.

int printf(const char *, ...);
void exit(int);

void PrintToConsole(int num) {
    printf("%d\n", num);
    exit(0);
}

Na verdade, o compilador tem que assumir que qualquer função arbitrária que o compilador não sabe o que faz pode sair ou lançar uma exceção (no caso do C ++). Você pode notar que *((char*)NULL) = 0;não será executado, pois a execução não continuará após a PrintToConsolechamada.

O comportamento indefinido ataca quando PrintToConsolerealmente retorna. O compilador espera que isso não aconteça (pois isso faria com que o programa executasse um comportamento indefinido, não importa o que acontecesse), portanto, tudo pode acontecer.

No entanto, vamos considerar outra coisa. Digamos que estejamos fazendo uma verificação de nulos e use a variável após a verificação de nulos.

int putchar(int);

const char *warning;

void lol_null_check(const char *pointer) {
    if (!pointer) {
        warning = "pointer is null";
    }
    putchar(*pointer);
}

Nesse caso, é fácil perceber que lol_null_checkrequer um ponteiro não NULL. A atribuição à warningvariável global não volátil não é algo que poderia sair do programa ou lançar qualquer exceção. O pointertambém não é volátil, portanto, não pode mudar magicamente seu valor no meio da função (se mudar, é um comportamento indefinido). A chamada lol_null_check(NULL)irá causar um comportamento indefinido que pode fazer com que a variável não seja atribuída (porque neste ponto, o fato de que o programa executa o comportamento indefinido é conhecido).

No entanto, o comportamento indefinido significa que o programa pode fazer qualquer coisa. Portanto, nada impede o comportamento indefinido de voltar no tempo e travar seu programa antes da int main()execução da primeira linha . É um comportamento indefinido, não precisa fazer sentido. Pode muito bem travar após digitar 3, mas o comportamento indefinido voltará no tempo e travará antes mesmo de você digitar 3. E quem sabe, talvez o comportamento indefinido substituirá a RAM do sistema e fará com que o sistema trave 2 semanas depois, enquanto seu programa indefinido não está em execução.

Konrad Borowski
fonte
Todos os pontos válidos. PrintToConsoleé minha tentativa de inserir um efeito colateral externo ao programa que é visível mesmo após travamentos e é fortemente sequenciado. Eu queria criar uma situação em que possamos dizer com certeza se esta declaração foi otimizada. Mas você está certo quanto ao fato de que pode nunca mais voltar Seu exemplo de gravação para um global pode estar sujeito a outras otimizações não relacionadas ao UB. Por exemplo, um global não utilizado pode ser excluído. Você tem uma ideia para criar um efeito colateral externo de uma forma que garanta o retorno do controle?
usr
Podem quaisquer efeitos colaterais observáveis ​​no mundo externo ser produzidos por código que um compilador estaria livre para assumir retornos? Pelo meu entendimento, mesmo um método que simplesmente lê uma volatilevariável poderia legitimamente acionar uma operação de E / S que poderia, por sua vez, interromper imediatamente o thread atual; o manipulador de interrupção pode então matar o thread antes que ele tenha a chance de realizar qualquer outra coisa. Não vejo nenhuma justificativa pela qual o compilador poderia empurrar o comportamento indefinido antes desse ponto.
supercat de
Do ponto de vista do padrão C, não haveria nada de ilegal em fazer o Comportamento Indefinido fazer com que o computador enviasse uma mensagem para algumas pessoas que rastreariam e destruiriam todas as evidências das ações anteriores do programa, mas se uma ação pudesse encerrar um thread, então tudo o que é sequenciado antes dessa ação teria que acontecer antes de qualquer comportamento indefinido que ocorresse depois dela.
supercat de
1

Se o programa atinge uma instrução que invoca um comportamento indefinido, nenhum requisito é colocado em qualquer saída / comportamento do programa; não importa se eles ocorreriam "antes" ou "depois" de um comportamento indefinido ser invocado.

Seu raciocínio sobre todos os três trechos de código está correto. Em particular, um compilador pode tratar qualquer instrução que invoca incondicionalmente um comportamento indefinido da forma como o GCC trata __builtin_unreachable(): como uma dica de otimização de que a instrução é inalcançável (e, portanto, todos os caminhos de código que levam incondicionalmente a ela também são inalcançáveis). É claro que outras otimizações semelhantes são possíveis.

R .. GitHub PARAR DE AJUDAR O GELO
fonte
1
Por curiosidade, quando __builtin_unreachable()começou a ter efeitos que recuaram e avançaram no tempo? Dado algo como extern volatile uint32_t RESET_TRIGGER; void RESET(void) { RESET_TRIGGER = 0xAA55; __memorybarrier(); __builtin_unreachable(); }eu poderia considerar que o builtin_unreachable()é bom deixar o compilador saber que ele pode omitir a returninstrução, mas isso seria bem diferente de dizer que o código anterior poderia ser omitido.
supercat
@supercat, uma vez que RESET_TRIGGER é volátil, a gravação nesse local pode ter efeitos colaterais arbitrários. Para o compilador, é como uma chamada de método opaca. Portanto, não pode ser provado (e não é o caso) que __builtin_unreachablefoi alcançado. Este programa está definido.
usr
@usr: Eu acho que os compiladores de baixo nível deveriam tratar os acessos voláteis como chamadas de método opacas, mas nem clang nem gcc o fazem. Entre outras coisas, uma chamada de método opaco pode fazer com que todos os bytes de para qualquer objeto cujo endereço tenha sido exposto ao mundo externo e que não foi nem será acessado por um restrictponteiro ativo, sejam escritos usando um unsigned char*.
supercat de
@usr: Se um compilador não tratar um acesso volátil como uma chamada de método opaca em relação a acessos a objetos expostos, não vejo nenhuma razão particular para esperar que isso seja feito para outros fins. O Padrão não exige que as implementações o façam, porque existem algumas plataformas de hardware onde um compilador pode ser capaz de conhecer todos os efeitos possíveis de um acesso volátil. Um compilador adequado para uso embutido, entretanto, deve reconhecer que acessos voláteis podem acionar hardware que não foi inventado quando o compilador foi escrito.
supercat de
@supercat Acho que você está certo. Parece que as operações voláteis "não têm efeito na máquina abstrata" e, portanto, não podem encerrar o programa ou causar efeitos colaterais.
usr
1

Muitos padrões para muitos tipos de coisas gastam muito esforço na descrição de coisas que as implementações DEVEM ou NÃO devem fazer, usando nomenclatura semelhante àquela definida na IETF RFC 2119 (embora não necessariamente citando as definições naquele documento). Em muitos casos, as descrições das coisas que as implementações devem fazer, exceto nos casos em que seriam inúteis ou impraticáveis, são mais importantes do que os requisitos aos quais todas as implementações em conformidade devem estar em conformidade.

Infelizmente, os padrões C e C ++ tendem a evitar descrições de coisas que, embora não sejam 100% exigidas, devem ser esperadas de implementações de qualidade que não documentam comportamento contrário. Uma sugestão de que as implementações devem fazer algo pode ser vista como implicando que aquelas que não são inferiores, e nos casos em que geralmente seria óbvio quais comportamentos seriam úteis ou práticos, versus impraticáveis ​​e inúteis, em uma determinada implementação, havia pouca necessidade percebida de o Padrão interferir em tais julgamentos.

Um compilador inteligente poderia estar em conformidade com o Padrão, eliminando qualquer código que não teria efeito, exceto quando o código recebe entradas que inevitavelmente causariam comportamento indefinido, mas "inteligente" e "burro" não são antônimos. O fato de os autores do Padrão decidirem que pode haver alguns tipos de implementações em que se comportar de maneira útil em uma determinada situação seria inútil e impraticável, não implica qualquer julgamento se tais comportamentos devem ser considerados práticos e úteis para os outros. Se uma implementação pudesse manter uma garantia comportamental sem nenhum custo além da perda de uma oportunidade de poda de "galho morto", quase qualquer valor que o código de usuário poderia receber dessa garantia excederia o custo de fornecê-la. A eliminação de galhos mortos pode ser boa nos casos em que não exigiria desistência, mas se em uma determinada situação o código do usuário pudesse ter manipulado quase qualquer comportamento possível, exceto a eliminação do ramo morto, qualquer esforço que o código do usuário tivesse que despender para evitar o UB provavelmente excederia o valor obtido do DBE.

supergato
fonte
É um bom ponto que evitar UB pode impor um custo ao código do usuário.
usr
@usr: É um ponto que os modernistas perdem completamente. Devo adicionar um exemplo? Por exemplo, se o código precisa avaliar x*y < zquando x*ynão estourou, e em caso de estouro produz 0 ou 1 de forma arbitrária, mas sem efeitos colaterais, não há razão na maioria das plataformas para que atender ao segundo e terceiro requisitos deva ser mais caro do que atender ao primeiro, mas qualquer maneira de escrever a expressão para garantir o comportamento definido pelo padrão em todos os casos, em alguns casos, adicionaria um custo significativo. Escrever a expressão (int64_t)x*y < zpoderia mais do que quadruplicar o custo de computação ...
supercat
... em algumas plataformas, e gravá-lo como (int)((unsigned)x*y) < zevitaria que um compilador empregue o que poderia ter sido substituições algébricas úteis (por exemplo, se ele souber disso xe zfor igual e positivo, pode simplificar a expressão original para y<0, mas a versão usando não sinal forçaria o compilador a realizar a multiplicação). Se o compilador puder garantir, mesmo que o padrão não o obrigue, ele manterá o requisito "rendimento 0 ou 1 sem efeitos colaterais", o código do usuário pode dar ao compilador oportunidades de otimização que de outra forma não poderia obter.
supercat
Sim, parece que alguma forma mais branda de comportamento indefinido seria útil aqui. O programador pode ativar um modo que faz x*ycom que seja emitido um valor normal em caso de estouro, mas qualquer valor. UB configurável em C / C ++ parece importante para mim.
usr
@usr: Se os autores do Padrão C89 estivessem sendo verdadeiros ao dizer que a promoção de valores curtos não assinados para assinados era a mudança de ruptura mais séria, e não fossem tolos ignorantes, isso implicaria que eles esperavam que, nos casos em que as plataformas Ao definirem garantias comportamentais úteis, as implementações para tais plataformas tornavam essas garantias disponíveis aos programadores e os programadores as exploravam, os compiladores para tais plataformas continuariam a oferecer tais garantias comportamentais quer o Padrão as ordenasse ou não .
supercat de