Est-il possible pour un #include manquant de casser le programme au moment de l'exécution?

31

Existe-t-il un cas où le fait de manquer un #includecasserait le logiciel au moment de l'exécution, alors que la construction continue?

En d'autres termes, est-il possible que

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

et

complexLogic();
cleverAlgorithms();

serait à la fois construire avec succès, mais se comporter différemment?

Antti_M
la source
1
Probablement avec vos inclusions, vous pourriez apporter à votre code des structures redéfinies différentes de celles utilisées par l'implémentation des fonctions. Cela peut entraîner une incompatibilité binaire. De telles situations ne peuvent pas être gérées par le compilateur et par l'éditeur de liens.
armagedescu
11
C'est certainement. Il est assez facile d'avoir des macros définies dans un en-tête qui changent complètement la signification du code qui vient après que cet en-tête soit #included.
Peter
4
Je suis sûr que Code Golf a fait au moins un défi sur cette base.
Mark
6
Je voudrais citer un exemple concret spécifique: la bibliothèque VLD pour la détection des fuites de mémoire. Lorsqu'un programme se termine avec VLD actif, il imprimera toutes les fuites de mémoire détectées sur un canal de sortie. Vous l'intégrez dans un programme en vous connectant à la bibliothèque VLD et en plaçant une seule ligne de #include <vld.h>dans une position stratégique dans votre code. La suppression ou l'ajout de cet en-tête VLD ne "casse" pas le programme, mais il affecte considérablement le comportement d'exécution. J'ai vu VLD ralentir un programme au point qu'il est devenu inutilisable.
Haliburton

Réponses:

40

Oui, c'est parfaitement possible. Je suis sûr qu'il existe de nombreuses façons, mais supposons que le fichier include contienne une définition de variable globale qui a appelé un constructeur. Dans le premier cas, le constructeur s'exécute et dans le second, il ne s'exécute pas.

Mettre une définition de variable globale dans un fichier d'en-tête est un mauvais style, mais c'est possible.

John
la source
1
<iostream>dans la bibliothèque standard fait précisément cela; si une unité de traduction inclut <iostream>alors l' std::ios_base::Initobjet statique sera construit au démarrage du programme, initialisant les flux de caractères, std::coutetc., sinon ce ne sera pas le cas.
ecatmur
33

Oui, c'est possible.

Tout ce qui concerne #includes se produit au moment de la compilation. Mais au moment de la compilation, les choses peuvent changer le comportement à l'exécution, bien sûr:

some/code.h:

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

puis

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

Avec le #include, la résolution de surcharge trouve le plus approprié foo(int)et imprime donc à la 1place de 2. De plus, puisque FOOest défini, il imprime en outre FOO.

Ce ne sont que deux exemples (non liés) qui me sont venus à l'esprit immédiatement, et je suis sûr qu'il y en a beaucoup plus.

pasbi
la source
14

Juste pour souligner le cas trivial, les directives de précompilateur:

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

Et alors

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

C'est pathologique, peut-être, mais un cas connexe s'est produit:

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

using namespace std;

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

Ça a l'air inoffensif. Essaie d'appeler std::max. Cependant, windows.h définit max comme

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

Si tel était le cas std::max, ce serait un appel de fonction normal qui évalue f () une fois et g () une fois. Mais avec windows.h là-dedans, il évalue maintenant f () ou g () deux fois: une fois pendant la comparaison et une fois pour obtenir la valeur de retour. Si f () ou g () n'était pas idempotent, cela peut provoquer des problèmes. Par exemple, si l'un d'eux se trouve être un compteur qui renvoie un nombre différent à chaque fois ...

Cort Ammon
la source
+1 pour avoir appelé la fonction max de Windows, un exemple réel d'inclure le mal d'implémentation et un fléau pour la portabilité partout.
Scott M
3
OTOH, si vous vous débarrassez using namespace std;et utilisez std::max(f(),g());, le compilateur va attraper le problème (avec un message obscur, mais au moins pointant vers le site d'appel).
Ruslan
@Ruslan Oh, oui. Si vous en avez l'occasion, c'est le meilleur plan. Mais parfois, on travaille avec du code hérité ... (non ... pas amer. Pas amer du tout!)
Cort Ammon
4

Il est possible de manquer une spécialisation de modèle.

// 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
la source
4

Incompatibilité binaire, accès à un membre ou pire encore, appel d'une fonction de la mauvaise classe:

#pragma once

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

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

#endif

Une fonction l'utilise, et c'est ok:

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

Apporter une autre version de la classe:

#pragma once

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

class class_w
{
public: int a;
};

#endif

En utilisant les fonctions principales, la deuxième définition modifie la définition de classe. Cela conduit à une incompatibilité binaire et se bloque simplement lors de l'exécution. Et corrigez le problème en supprimant le premier include dans 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;
}

Aucune des variantes ne génère une erreur de temps de compilation ou de liaison.

La situation vice versa, l'ajout d'une inclusion corrige le crash:

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

Ces situations sont encore plus difficiles à résoudre lors de la correction de bogues dans une ancienne version du programme ou lors de l'utilisation d'une bibliothèque externe / dll / objet partagé. C'est pourquoi il faut parfois respecter les règles de compatibilité descendante binaire.

armagedescu
la source
Le deuxième en-tête ne sera pas inclus en raison de ifndef. Sinon, il ne se compilera pas (la redéfinition de classe n'est pas autorisée).
Igor R.
@IgorR. Être attentif. Le deuxième en-tête (include1.h) est le seul inclus dans le premier code source. Cela conduit à une incompatibilité binaire. C'est exactement le but du code, pour illustrer comment une inclusion peut conduire à un crash lors de l'exécution.
armagedescu
1
@IgorR. il s'agit d'un code très simpliste, qui illustre une telle situation. Mais dans la réalité, la situation peut être beaucoup plus complexe et subtile. Essayez de patcher un programme sans réinstaller le package entier. C'est la situation typique où il faut suivre strictement les règles de compatibilité binaire en amont. Sinon, l'application de correctifs est une tâche impossible.
armagedescu
Je ne sais pas ce qu'est "le premier code source", mais si vous voulez dire que 2 unités de traduction ont 2 définitions différentes d'une classe, c'est une violation ODR, c'est-à-dire un comportement indéfini.
Igor R.
1
Il s'agit d' un comportement non défini , comme décrit par la norme C ++. FWIW, bien sûr, il est possible de provoquer un UB de cette façon ...
Igor R.
3

Je tiens à souligner que le problème existe également en C.

Vous pouvez dire au compilateur qu'une fonction utilise une convention d'appel. Si vous ne le faites pas, le compilateur devra deviner qu'il utilise celui par défaut, contrairement à C ++ où le compilateur peut refuser de le compiler.

Par exemple,

main.c

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

foo.c

#include <stdio.h>

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

Sous Linux sur x86-64, ma sortie est

0

Si vous omettez le prototype ici, le compilateur suppose que vous avez

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

Et la convention pour les listes d'arguments non spécifiées exige que cela floatsoit converti doublepour être passé. Donc, bien que j'aie donné 1.0f, le compilateur le convertit 1.0dpour le transmettre foo. Et selon le supplément du processeur d'architecture AMD64 de l'interface binaire d'application System V, le doubleobtient est transmis dans les 64 bits les moins significatifs de xmm0. Mais fooattend un flottant, et il le lit à partir des 32 bits les moins significatifs de xmm0, et obtient 0.

izmw1cfg
la source