É possível que um #include ausente interrompa o programa em tempo de execução?

31

Existe algum caso em que a falta de um #includeinterrompa o software em tempo de execução, enquanto a compilação ainda continua?

Em outras palavras, é possível que

#include "some/code.h"
complexLogic();
cleverAlgorithms();

e

complexLogic();
cleverAlgorithms();

ambos construiriam com sucesso, mas se comportariam de maneira diferente?

Antti_M
fonte
11
Provavelmente, com suas inclusões, você poderia trazer estruturas redefinidas em código diferentes das usadas pela implementação de funções. Isso pode levar à incompatibilidade binária. Tais situações não podem ser tratadas pelo compilador e pelo vinculador.
armagedescu 20/03
11
Certamente é. É muito fácil ter macros definidas em um cabeçalho que alteram completamente o significado do código que vem depois desse cabeçalho ser #included.
Peter
4
Tenho certeza de que o Code Golf fez pelo menos um desafio com base nisso.
Mark
6
Gostaria de destacar um exemplo específico do mundo real: A biblioteca VLD para detecção de vazamento de memória. Quando um programa termina com o VLD ativo, ele imprime todos os vazamentos de memória detectados em algum canal de saída. Você o integra a um programa vinculando-se à biblioteca VLD e colocando uma única linha #include <vld.h>em uma posição estratégica no seu código. A remoção ou adição desse cabeçalho de VLD não "interrompe" o programa, mas afeta significativamente o comportamento do tempo de execução. Vi o VLD diminuir a velocidade de um programa a ponto de tornar-se inutilizável.
Haliburton

Respostas:

40

Sim, é perfeitamente possível. Tenho certeza de que existem várias maneiras, mas suponha que o arquivo de inclusão contivesse uma definição de variável global chamada de construtor. No primeiro caso, o construtor executaria, e no segundo, não.

Colocar uma definição de variável global em um arquivo de cabeçalho é um estilo pobre, mas é possível.

John
fonte
11
<iostream>na biblioteca padrão faz exatamente isso; se alguma unidade de conversão incluir <iostream>, o std::ios_base::Initobjeto estático será construído no início do programa, inicializando os fluxos de caracteres std::coutetc., caso contrário, não será.
ecatmur 22/03
33

Sim, isso é possível.

Tudo sobre #includes acontece em tempo de compilação. Mas, em tempo de compilação, as coisas podem mudar o comportamento em tempo de execução, é claro:

some/code.h:

#define FOO
int foo(int a) { return 1; }

então

#include <iostream>
int foo(float a) { return 2; }

#include "some/code.h"  // Remove that line

int main() {
  std::cout << foo(1) << std::endl;
  #ifdef FOO
    std::cout << "FOO" std::endl;
  #endif
}

Com o #include, a resolução de sobrecarga encontra o mais apropriado foo(int)e, portanto, imprime em 1vez de 2. Além disso, como FOOé definido, ele também imprime FOO.

São apenas dois exemplos (não relacionados) que vieram à minha mente imediatamente e tenho certeza de que há muito mais.

pasbi
fonte
14

Apenas para apontar o caso trivial, as diretivas de pré-compilador:

// main.cpp
#include <iostream>
#include "trouble.h" // comment this out to change behavior

bool doACheck(); // always returns true

int main()
{
    if (doACheck())
        std::cout << "Normal!" << std::endl;
    else
        std::cout << "BAD!" << std::endl;
}

E depois

// trouble.h
#define doACheck(...) false

É patológico, talvez, mas já tive um caso relacionado:

#include <algorithm>
#include <windows.h> // comment this out to change behavior

using namespace std;

double doThings()
{
    return max(f(), g());
}

Parece inócuo. Tenta ligar std::max. No entanto, windows.h define max como

#define max(a, b)  (((a) > (b)) ? (a) : (b))

Se fosse std::max, seria uma chamada de função normal que avalia f () uma vez eg () uma vez. Mas com windows.h, agora ele avalia f () ou g () duas vezes: uma vez durante a comparação e outra para obter o valor de retorno. Se f () ou g () não for idempotente, isso poderá causar problemas. Por exemplo, se um deles for um contador que retorna um número diferente toda vez ...

Cort Ammon
fonte
+1 por chamar a função max do Windows, um exemplo do mundo real de incluir implementação mal e uma proibição de portabilidade em qualquer lugar.
Scott M
3
OTOH, se você se livrar using namespace std;e usar std::max(f(),g());, o compilador detectará o problema (com uma mensagem obscura, mas pelo menos apontando para o site de chamada).
Ruslan
@Ruslan Oh, sim. Se tiver a chance, esse é o melhor plano. Mas às vezes alguém está trabalhando com código legado ... (não ... nem um pouco amargo! Nem um pouco amargo!)
Cort Ammon
4

É possível que esteja faltando uma especialização de modelo.

// header1.h:

template<class T>
void algorithm(std::vector<T> &ts) {
    // clever algorithm (sorting, for example)
}

class thingy {
    // stuff
};

// header2.h

template<>
void algorithm(std::vector<thingy> &ts) {
    // different clever algorithm
}

// main.cpp

#include <vector>
#include "header1.h"
//#include "header2.h"

int main() {
    std::vector<thingy> thingies;
    algorithm(thingies);
}
user253751
fonte
4

Incompatibilidade binária, acessando um membro ou, pior ainda, chamando uma função da classe errada:

#pragma once

//include1.h:
#ifndef classw
#define classw

class class_w
{
    public: int a, b;
};

#endif

Uma função usa e está tudo bem:

//functions.cpp
#include <include1.h>
void smartFunction(class_w& x){x.b = 2;}

Trazendo outra versão da classe:

#pragma once

//include2.h:
#ifndef classw
#define classw

class class_w
{
public: int a;
};

#endif

Usando funções em main, a segunda definição altera a definição da classe. Isso leva à incompatibilidade binária e simplesmente trava no tempo de execução. E corrija o problema removendo o primeiro include em main.cpp:

//main.cpp

#include <include2.h> //<-- Remove this to fix the crash
#include <include1.h>

void smartFunction(class_w& x);
int main()
{
    class_w w;
    smartFunction(w);
    return 0;
}

Nenhuma das variantes gera um erro de tempo de compilação ou link.

A situação vice-versa, adicionando um include, corrige a falha:

//main.cpp
//#include <include1.h>  //<-- Add this include to fix the crash
#include <include2.h>
...

Essas situações são ainda mais difíceis ao corrigir erros em uma versão antiga do programa ou ao usar um objeto externo library / dll / shared. É por isso que às vezes devem ser seguidas as regras de compatibilidade com versões anteriores binárias.

armagedescu
fonte
O segundo cabeçalho não será incluído devido ao ifndef. Caso contrário, ele não será compilado (redefinição de classe não é permitida).
Igor R.
@IgorR. Esteja atento. O segundo cabeçalho (include1.h) é o único incluído no primeiro código-fonte. Isso leva à incompatibilidade binária. Esse é exatamente o objetivo do código, para ilustrar como uma inclusão pode levar a uma falha no tempo de execução.
armagedescu 20/03
11
@IgorR. esse é um código muito simplista, que ilustra essa situação. Mas na situação da vida real pode ser muito mais complicado sutil. Tente corrigir algum programa sem reinstalar o pacote inteiro. É a situação típica em que devem ser seguidas estritamente as regras de compatibilidade binária reversa. Caso contrário, a aplicação de patches é uma tarefa impossível.
armagedescu 20/03
Não sei ao certo qual é o "primeiro código-fonte", mas se você quer dizer que 2 unidades de tradução têm 2 definições diferentes de uma classe, é uma violação do ODR, ou seja, comportamento indefinido.
Igor R.
11
Esse é um comportamento indefinido , conforme descrito pelo padrão C ++. FWIW, é claro, é possível causar um UB dessa maneira ...
Igor R.
3

Quero ressaltar que o problema também existe em C.

Você pode dizer ao compilador que uma função usa alguma convenção de chamada. Caso contrário, o compilador terá que adivinhar que usa o padrão, diferente do C ++, onde o compilador pode se recusar a compilá-lo.

Por exemplo,

main.c

int main(void) {
  foo(1.0f);
  return 1;
}

foo.c

#include <stdio.h>

void foo(float x) {
  printf("%g\n", x);
}

No Linux em x86-64, minha saída é

0

Se você omitir o protótipo aqui, o compilador assume que você possui

int foo(); // Has different meaning in C++

E a convenção para listas de argumentos não especificadas exige que floatdeve ser convertido doublepara ser aprovado. Então, embora eu tenha dado 1.0f, o compilador o converte 1.0dpara passar para foo. E de acordo com o Suplemento de processador de arquitetura AMD64 da interface binária de aplicativos do System V, as doublefalhas são passadas nos 64 bits menos significativos de xmm0. Mas fooespera um float e o lê dos 32 bits menos significativos de xmm0, e obtém 0.

izmw1cfg
fonte