Quais são os comportamentos indefinidos comuns que um programador de C ++ deve conhecer? [fechadas]

201

Quais são os comportamentos indefinidos comuns que um programador de C ++ deve conhecer?

Diga, como:

a[i] = i++;

yesraaj
fonte
3
Você tem certeza. Isso parece bem definido.
Martin York
17
6.2.2 ordem de avaliação [expr.evaluation] no C ++ linguagem de programação dizer so.I não tem qualquer outra referência
yesraaj
4
Ele está certo .. apenas olhou para 6.2.2 em The C ++ Programming Language and ele diz que v [i] = i ++ é indefinido
dancavallaro
4
Eu imaginaria porque o compilador executa o i ++ antes ou depois de calcular a localização da memória de v [i]. claro, eu sempre serei designado para lá. mas poderia escrever a qualquer v [i] ou v [i + 1], dependendo a ordem das operações ..
Evan Teran
2
Tudo o que a linguagem de programação C ++ diz é "A ordem das operações das subexpressões em uma expressão é indefinida. Em particular, você não pode assumir que a expressão é avaliada da esquerda para a direita".
Dancavallaro

Respostas:

233

Ponteiro

  • Desreferenciando um NULLponteiro
  • Desreferenciando um ponteiro retornado por uma "nova" alocação de tamanho zero
  • Usando ponteiros para objetos cuja vida útil terminou (por exemplo, empilhar objetos alocados ou objetos excluídos)
  • Desreferenciando um ponteiro que ainda não foi definitivamente inicializado
  • Executar aritmética de ponteiro que produz um resultado fora dos limites (acima ou abaixo) de uma matriz.
  • Desreferenciando o ponteiro em um local além do final de uma matriz.
  • Convertendo ponteiros em objetos de tipos incompatíveis
  • Usando memcpypara copiar buffers sobrepostos .

Estouros de buffer

  • Leitura ou gravação em um objeto ou matriz em um deslocamento negativo ou além do tamanho desse objeto (estouro de pilha / pilha)

Estouros Inteiros

  • Estouro de número inteiro assinado
  • Avaliando uma expressão que não é matematicamente definida
  • Valores de desvio à esquerda em uma quantidade negativa (os desvios à direita em valores negativos são definidos pela implementação)
  • Mudança de valores por uma quantidade maior ou igual ao número de bits no número (por exemplo, int64_t i = 1; i <<= 72é indefinido)

Tipos, Elenco e Const

  • Converter um valor numérico em um valor que não pode ser representado pelo tipo de destino (diretamente ou via static_cast)
  • Usando uma variável automática antes de ser definitivamente atribuída (por exemplo, int i; i++; cout << i;)
  • Usando o valor de qualquer objeto do tipo que não seja volatileou sig_atomic_tao receber um sinal
  • Tentativa de modificar um literal de cadeia ou qualquer outro objeto const durante sua vida útil
  • Concatenando um estreito com um literal de cadeia ampla durante o pré-processamento

Função e Modelo

  • Não retornando um valor de uma função de retorno de valor (diretamente ou saindo de um bloco de tentativa)
  • Várias definições diferentes para a mesma entidade (classe, modelo, enumeração, função embutida, função membro estática, etc.)
  • Recursão infinita na instanciação de modelos
  • Chamar uma função usando diferentes parâmetros ou vinculação aos parâmetros e vinculação que a função está definida como usando.

OOP

  • Destruições em cascata de objetos com duração de armazenamento estático
  • O resultado da atribuição a objetos parcialmente sobrepostos
  • Reinserir recursivamente uma função durante a inicialização de seus objetos estáticos
  • Fazendo chamadas de função virtual para funções virtuais puras de um objeto de seu construtor ou destruidor
  • Referindo-se a membros não estáticos de objetos que não foram construídos ou que já foram destruídos

Arquivo de origem e pré-processamento

  • Um arquivo de origem não vazio que não termina com uma nova linha ou termina com uma barra invertida (anterior ao C ++ 11)
  • Uma barra invertida seguida por um caractere que não faz parte dos códigos de escape especificados em uma constante de caractere ou seqüência de caracteres (isso é definido pela implementação no C ++ 11).
  • Excedendo os limites de implementação (número de blocos aninhados, número de funções em um programa, espaço disponível na pilha ...)
  • Valores numéricos do pré-processador que não podem ser representados por um long int
  • Diretiva de pré-processamento no lado esquerdo de uma definição de macro semelhante a função
  • Gerando dinamicamente o token definido em uma #ifexpressão

Para ser classificado

  • Chamada de saída durante a destruição de um programa com duração de armazenamento estático
Diomidis Spinellis
fonte
Hm ... NaN (x / 0) e Infinity (0/0) foram cobertos pelo IEE 754, se o C ++ foi projetado posteriormente, por que ele registra x / 0 como indefinido?
precisa saber é o seguinte
Re: "Uma barra invertida seguida por um caractere que não faz parte dos códigos de escape especificados em um caractere ou string constante." Isso é UB em C89 (§3.1.3.4) e C ++ 03 (que incorpora C89), mas não em C99. C99 diz que "o resultado não é um token e é necessário um diagnóstico" (§6.4.4.4). Presumivelmente, C ++ 0x (que incorpora C89) será o mesmo.
Adam Rosenfield
1
O padrão C99 possui uma lista de comportamentos indefinidos no apêndice J.2. Levaria algum trabalho para adaptar esta lista ao C ++. Você precisaria alterar as referências às cláusulas C ++ corretas, em vez das cláusulas C99, remover qualquer coisa irrelevante e também verificar se todas essas coisas realmente são indefinidas tanto em C ++ quanto em C. Mas isso fornece um começo.
21711 Steve Jobs (
1
@ new123456 - nem todas as unidades de ponto flutuante são compatíveis com IEE754. Se o C ++ exigisse conformidade com a IEE754, os compiladores precisariam testar e manipular o caso em que o RHS é zero por meio de uma verificação explícita. Ao tornar o comportamento indefinido, o compilador pode evitar essa sobrecarga dizendo "se você usar uma FPU que não seja IEE754, não obterá o comportamento da FPU IEEE754".
SecurityMatt
1
"Avaliando uma expressão cujo resultado não está no intervalo dos tipos correspondentes" .... o estouro de números inteiros está bem definido para os tipos integrais UNSIGNED, apenas os não assinados.
Nacitar sevaht
31

A ordem em que os parâmetros de função são avaliados é um comportamento não especificado . (Isso não fará o seu programa travar, explodir ou pedir pizza ... diferente do comportamento indefinido .)

O único requisito é que todos os parâmetros sejam totalmente avaliados antes que a função seja chamada.


Este:

// The simple obvious one.
callFunc(getA(),getB());

Pode ser equivalente a isso:

int a = getA();
int b = getB();
callFunc(a,b);

Ou isto:

int b = getB();
int a = getA();
callFunc(a,b);

Pode ser também; depende do compilador. O resultado pode importar, dependendo dos efeitos colaterais.

Martin York
fonte
23
O pedido não é especificado, não é indefinido.
Rob Kennedy
1
Eu odeio este :) Eu perdi um dia de trabalho, uma vez rastrear um desses casos ... de qualquer maneira aprendi minha lição e não ter caído novamente, felizmente
Robert Gould
2
@ Rob: Eu discutiria com você sobre a mudança de significado aqui, mas eu sei que o comitê de padrões é muito exigente quanto à definição exata dessas duas palavras. Então, eu vou mudar :-)
Martin York
2
Eu tive sorte com isso. Fui mordido por ele quando estava na faculdade e tive um professor que deu uma olhada e me contou o meu problema em cerca de 5 segundos. Não sei dizer quanto tempo eu teria perdido a depuração de outra forma.
Bill the Lizard
27

O compilador pode reordenar as partes da avaliação de uma expressão (assumindo que o significado não seja alterado).

Da pergunta original:

a[i] = i++;

// This expression has three parts:
(a) a[i]
(b) i++
(c) Assign (b) to (a)

// (c) is guaranteed to happen after (a) and (b)
// But (a) and (b) can be done in either order.
// See n2521 Section 5.17
// (b) increments i but returns the original value.
// See n2521 Section 5.2.6
// Thus this expression can be written as:

int rhs  = i++;
int lhs& = a[i];
lhs = rhs;

// or
int lhs& = a[i];
int rhs  = i++;
lhs = rhs;

Bloqueio verificado duas vezes. E um erro fácil de cometer.

A* a = new A("plop");

// Looks simple enough.
// But this can be split into three parts.
(a) allocate Memory
(b) Call constructor
(c) Assign value to 'a'

// No problem here:
// The compiler is allowed to do this:
(a) allocate Memory
(c) Assign value to 'a'
(b) Call constructor.
// This is because the whole thing is between two sequence points.

// So what is the big deal.
// Simple Double checked lock. (I know there are many other problems with this).
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        a = new A("Plop");  // (Point A).
    }
}
a->doStuff();

// Think of this situation.
// Thread 1: Reaches point A. Executes (a)(c)
// Thread 1: Is about to do (b) and gets unscheduled.
// Thread 2: Reaches point B. It can now skip the if block
//           Remember (c) has been done thus 'a' is not NULL.
//           But the memory has not been initialized.
//           Thread 2 now executes doStuff() on an uninitialized variable.

// The solution to this problem is to move the assignment of 'a'
// To the other side of the sequence point.
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        A* tmp = new A("Plop");  // (Point A).
        a = tmp;
    }
}
a->doStuff();

// Of course there are still other problems because of C++ support for
// threads. But hopefully these are addresses in the next standard.
Martin York
fonte
o que quer dizer com ponto de sequência?
yesraaj
1
Ooh ... isso é desagradável, especialmente desde que eu vi que a estrutura exata recomendada em Java
Tom
Observe que alguns compiladores definem o comportamento nessa situação. No VC ++ 2005+, por exemplo, se a é volátil, os bariers de memória necessários são configurados para impedir a reordenação das instruções, para que o bloqueio com verificação dupla funcione.
Eclipse
Martin York: // (c) é garantido que ocorra após (a) e (b) </i> É? É certo que, nesse exemplo em particular, o único cenário em que isso poderia importar seria se 'i' fosse uma variável volátil mapeada para um registro de hardware, e um [i] (valor antigo de 'i') fosse aliasado a ele, mas existe alguma garantir que o incremento ocorrerá antes de um ponto de sequência?
Supercat
5

O meu favorito é "Recursão infinita na instanciação de modelos" porque acredito que é o único onde o comportamento indefinido ocorre no momento da compilação.

Daniel Earwicker
fonte
Já fiz isso antes, mas não vejo como é indefinido. É bastante óbvio que você está fazendo uma recursão infinita em uma reflexão tardia.
Robert Gould
O problema é que o compilador não pode examinar seu código e decidir com precisão se ele sofrerá recursão infinita ou não. É uma instância do problema de parada. Veja: stackoverflow.com/questions/235984/…
Daniel Earwicker
Sim sua definitivamente um problema da parada
Robert Gould
fez meu sistema travar por causa da troca causada por pouca memória.
Johannes Schaub - litb 28/12/08
2
As constantes do pré-processador que não se encaixam em um int também são tempo de compilação.
Joshua
5

Atribuindo a uma constante após remover o constness usando const_cast<>:

const int i = 10; 
int *p =  const_cast<int*>( &i );
*p = 1234; //Undefined
yesraaj
fonte
5

Além do comportamento indefinido , há também o comportamento definido pela implementação igualmente desagradável .

O comportamento indefinido ocorre quando um programa faz algo cujo resultado não é especificado pelo padrão.

O comportamento definido pela implementação é uma ação de um programa cujo resultado não é definido pelo padrão, mas que a implementação é requerida para documentar. Um exemplo é "Literais de caracteres multibyte", da questão Stack Overflow. Existe um compilador C que falha ao compilar isso? .

O comportamento definido pela implementação só o incomoda quando você começa a portar (mas a atualização para a nova versão do compilador também está portando!)

Constantin
fonte
4

As variáveis ​​podem ser atualizadas apenas uma vez em uma expressão (tecnicamente uma vez entre os pontos de sequência).

int i =1;
i = ++i;

// Undefined. Assignment to 'i' twice in the same expression.
Martin York
fonte
Infecte pelo menos uma vez entre dois pontos de sequência.
Prasoon Saurav
2
@ Pasoon: Eu acho que você quis dizer: no máximo uma vez entre dois pontos de sequência. :-)
Nawaz
3

Uma compreensão básica dos vários limites ambientais. A lista completa está na seção 5.2.4.1 da especificação C. Aqui estão alguns;

  • 127 parâmetros em uma definição de função
  • 127 argumentos em uma chamada de função
  • 127 parâmetros em uma definição de macro
  • 127 argumentos em uma chamada de macro
  • 4095 caracteres em uma linha de origem lógica
  • 4095 caracteres em uma literal de cadeia de caracteres ou literal de cadeia ampla (após concatenação)
  • 65535 bytes em um objeto (apenas em um ambiente hospedado)
  • 15 níveis de anestesia para arquivos #included
  • 1023 rótulos de maiúsculas e minúsculas para uma instrução switch (excluindo os de qualquer instrução switch aninhada)

Na verdade, fiquei um pouco surpreso com o limite de 1023 rótulos de caso para uma instrução switch, posso prever que sendo excedido o código / lex / parsers gerado com bastante facilidade.

Se esses limites forem excedidos, você terá um comportamento indefinido (falhas, falhas de segurança, etc.).

Certo, eu sei que isso é da especificação C, mas o C ++ compartilha esses suportes básicos.

RandomNickName42
fonte
9
Se você atingir esses limites, terá mais problemas do que comportamentos indefinidos.
precisa saber é o seguinte
É FACILMENTE poderia exceder 65535 bytes de um objecto, tal como uma DST :: vector
Demi
2

Usando memcpypara copiar entre regiões de memória sobrepostas. Por exemplo:

char a[256] = {};
memcpy(a, a, sizeof(a));

O comportamento é indefinido de acordo com o Padrão C, incluído no Padrão C ++ 03.

7.21.2.1 A função memcpy

Sinopse

1 / #include void * memcpy (void * restrita s1, const void * restrita s2, size_t n);

Descrição

2 / A função memcpy copia n caracteres do objeto apontado por s2 para o objeto apontado por s1. Se a cópia ocorrer entre objetos que se sobrepõem, o comportamento é indefinido. Retorna 3 A função memcpy retorna o valor de s1.

7.21.2.2 A função memmove

Sinopse

1 #include void * memmove (void * s1, const void * s2, size_t n);

Descrição

2 A função memmove copia n caracteres do objeto apontado por s2 para o objeto apontado por s1. A cópia ocorre como se os n caracteres do objeto apontado por s2 fossem primeiro copiados para uma matriz temporária de n caracteres que não se sobrepusessem aos objetos apontados por s1 e s2 e, em seguida, os n caracteres da matriz temporária fossem copiados para o objeto apontado por s1. Devoluções

3 A função memmove retorna o valor de s1.

John Dibling
fonte
2

O único tipo para o qual o C ++ garante um tamanho é char. E o tamanho é 1. O tamanho de todos os outros tipos depende da plataforma.

JaredPar
fonte
Não é para isso que serve <cstdint>? Ele define tipos como uint16_6 e outros.
Jasper Bekkers
Sim, mas o tamanho da maioria dos tipos, por exemplo, longo, não está bem definido.
JaredPar
O cstdint também não faz parte do padrão atual do c ++. consulte boost / stdint.hpp para obter uma solução atualmente portátil.
Evan Teran
Esse não é um comportamento indefinido. O padrão diz que a plataforma em conformidade define os tamanhos, em vez do padrão que os define.
Daniel Earwicker
1
@JaredPar: É um post complexo, com muitos tópicos de conversa, então resumi tudo aqui . A conclusão é a seguinte: "5. Para representar -2147483647 e +2147483647 em binário, você precisa de 32 bits."
John Dibling
2

Objetos no nível de namespace em unidades de compilação diferentes nunca devem depender um do outro para inicialização, porque sua ordem de inicialização é indefinida.

yesraaj
fonte