¿Es posible que un #include faltante rompa el programa en tiempo de ejecución?

31

¿Hay algún caso en el que la falta de un #includese rompa el software en tiempo de ejecución, mientras que la compilación aún continúa?

En otras palabras, ¿es posible que

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

y

complexLogic();
cleverAlgorithms();

¿ambos construirían con éxito, pero se comportarían de manera diferente?

Antti_M
fuente
1
Probablemente, con sus inclusiones, podría traer a su código estructuras redefinidas que son diferentes a las utilizadas por la implementación de funciones. Esto puede conducir a incompatibilidad binaria. Tales situaciones no pueden ser manejadas por el compilador y por el enlazador.
Armagedescu
11
Ciertamente lo es. Es bastante fácil tener macros definidas en un encabezado que cambien completamente el significado del código que viene después de que ese encabezado es #included.
Peter
44
Estoy seguro de que Code Golf ha hecho al menos un desafío basado en esto.
Mark
66
Me gustaría señalar un ejemplo específico del mundo real: la biblioteca VLD para la detección de pérdidas de memoria. Cuando un programa termina con VLD activo, imprimirá todas las pérdidas de memoria detectadas en algún canal de salida. Lo integra en un programa al vincular a la biblioteca VLD y colocar una sola línea #include <vld.h>en una posición estratégica en su código. Eliminar o agregar ese encabezado de VLD no "interrumpe" el programa, pero afecta significativamente el comportamiento del tiempo de ejecución. He visto a VLD desacelerar un programa hasta el punto de que se volvió inutilizable.
Haliburton

Respuestas:

40

Sí, es perfectamente posible. Estoy seguro de que hay muchas formas, pero supongamos que el archivo de inclusión contiene una definición de variable global que se denomina constructor. En el primer caso, el constructor se ejecutaría, y en el segundo no.

Poner una definición de variable global en un archivo de encabezado es un estilo pobre, pero es posible.

Juan
fuente
1
<iostream>en la biblioteca estándar hace precisamente esto; si alguna unidad de traducción incluye, <iostream>entonces el std::ios_base::Initobjeto estático se construirá al inicio del programa, inicializando las secuencias de caracteres std::cout, etc., de lo contrario no lo hará.
ecatmur
33

Si, eso es posible.

Todo lo que concierne a #includes sucede en tiempo de compilación. Pero las cosas en tiempo de compilación pueden cambiar el comportamiento en tiempo de ejecución, por supuesto:

some/code.h:

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

entonces

#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
}

Con la #includeresolución de sobrecarga encuentra el más apropiado foo(int)y por lo tanto se imprime 1en lugar de 2. Además, como FOOestá definido, también imprime FOO.

Son solo dos ejemplos (no relacionados) que me vinieron a la mente de inmediato, y estoy seguro de que hay muchos más.

pasbi
fuente
14

Solo para señalar el caso trivial, las directivas de precompilador:

// 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;
}

Y entonces

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

Es patológico, tal vez, pero he tenido un caso relacionado:

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

using namespace std;

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

Parece inocuo Intenta llamar std::max. Sin embargo, windows.h define max para ser

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

Si esto fuera así std::max, sería una llamada de función normal que evalúa f () una vez y g () una vez. Pero con windows.h allí, ahora evalúa f () o g () dos veces: una durante la comparación y otra para obtener el valor de retorno. Si f () o g () no fue idempotente, esto puede causar problemas. Por ejemplo, si uno de ellos es un contador que devuelve un número diferente cada vez ...

Cort Ammon
fuente
+1 para llamar a la función máxima de Windows, un ejemplo del mundo real de incluir el mal de implementación y una ruina para la portabilidad en todas partes.
Scott M
3
OTOH, si se deshace using namespace std;y usa std::max(f(),g());, el compilador detectará el problema (con un mensaje oscuro, pero al menos apuntando al sitio de la llamada).
Ruslan
@Ruslan Oh, sí. Si se le da la oportunidad, ese es el mejor plan. Pero a veces uno está trabajando con código heredado ... (no ... ¡no es amargo, no es amargo en absoluto!)
Cort Ammon
4

Es posible que falte una especialización de plantilla.

// 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);
}
usuario253751
fuente
4

Incompatibilidad binaria, acceder a un miembro o, lo que es peor, llamar a una función de la clase incorrecta:

#pragma once

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

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

#endif

Una función lo usa, y está bien:

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

Trayendo otra versión de la clase:

#pragma once

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

class class_w
{
public: int a;
};

#endif

Usando funciones en main, la segunda definición cambia la definición de clase. Conduce a incompatibilidad binaria y simplemente se bloquea en tiempo de ejecución. Y solucione el problema eliminando la primera inclusión en 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;
}

Ninguna de las variantes genera un error de tiempo de compilación o enlace.

La situación viceversa, al agregar un include, corrige el bloqueo:

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

Estas situaciones son aún más difíciles cuando se corrige un error en una versión antigua del programa o cuando se usa una biblioteca externa / dll / objeto compartido. Es por eso que a veces se deben seguir las reglas de compatibilidad binaria hacia atrás.

armagedescu
fuente
El segundo encabezado no se incluirá debido a ifndef. De lo contrario, no se compilará (no se permite la redefinición de clases).
Igor R.
@IgorR. Ser atento. El segundo encabezado (include1.h) es el único incluido en el primer código fuente. Esto lleva a incompatibilidad binaria. Ese es exactamente el propósito del código, ilustrar cómo una inclusión puede provocar un bloqueo en tiempo de ejecución.
Armagedescu
1
@IgorR. Este es un código muy simplista, que ilustra tal situación. Pero en la vida real, la situación puede ser mucho más complicada. Intenta parchear algún programa sin reinstalar todo el paquete. Es la situación típica donde se deben seguir estrictamente las reglas de compatibilidad binaria hacia atrás. De lo contrario, parchar es una tarea imposible.
Armagedescu
No estoy seguro de cuál es "el primer código fuente", pero si quiere decir que 2 unidades de traducción tienen 2 definiciones diferentes de una clase, es una violación de ODR, es decir, un comportamiento indefinido.
Igor R.
1
Ese es un comportamiento indefinido , como lo describe el Estándar C ++. FWIW, por supuesto, es posible causar un UB de esta manera ...
Igor R.
3

Quiero señalar que el problema también existe en C.

Puede decirle al compilador que una función usa alguna convención de llamada. Si no lo hace, el compilador tendrá que adivinar que usa el predeterminado, a diferencia de C ++, donde el compilador puede negarse a compilarlo.

Por ejemplo,

main.c

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

foo.c

#include <stdio.h>

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

En Linux en x86-64, mi salida es

0

Si omite el prototipo aquí, el compilador asume que tiene

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

Y la convención para listas de argumentos no especificadas requiere que floatse convierta para doubleque se pase. Entonces, aunque di 1.0f, el compilador lo convierte 1.0dpara pasarlo foo. Y de acuerdo con el Suplemento del procesador de arquitectura AMD64 de la interfaz binaria de la aplicación System V, doublese pasa en los 64 bits menos significativos xmm0. Pero fooespera un flotante, y lo lee de los 32 bits menos significativos de xmm0, y obtiene 0.

izmw1cfg
fuente