É 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?


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.


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.


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 ...


+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);
}

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.


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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.