Kann ein fehlendes #include das Programm zur Laufzeit unterbrechen?

31

Gibt es einen Fall, in dem ein Fehlen #includedie Software zur Laufzeit beschädigen würde, während der Build noch läuft?

Mit anderen Worten, ist es möglich, dass

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

und

complexLogic();
cleverAlgorithms();

würden beide erfolgreich bauen, sich aber unterschiedlich verhalten?

Antti_M
quelle
1
Wahrscheinlich könnten Sie mit Ihren Includes in Ihrem Code neu definierte Strukturen einbringen, die sich von denen unterscheiden, die bei der Implementierung von Funktionen verwendet werden. Dies kann zu einer binären Inkompatibilität führen. Solche Situationen können weder vom Compiler noch vom Linker behandelt werden.
armagedescu
11
Es ist sicherlich. Es ist ziemlich einfach, Makros in einem Header zu definieren, die die Bedeutung des Codes, der nach dem Header #included kommt, vollständig ändern .
Peter
4
Ich bin sicher, dass Code Golf auf dieser Grundlage mindestens eine Herausforderung gemeistert hat.
Mark
6
Ich möchte auf ein konkretes Beispiel aus der Praxis hinweisen: Die VLD-Bibliothek zur Erkennung von Speicherverlusten. Wenn ein Programm mit aktivierter VLD beendet wird, druckt es alle erkannten Speicherlecks auf einem Ausgangskanal aus. Sie integrieren es in ein Programm, indem Sie eine Verknüpfung zur VLD-Bibliothek herstellen und eine einzelne Zeile #include <vld.h>an einer strategischen Position in Ihrem Code platzieren. Das Entfernen oder Hinzufügen dieses VLD-Headers "bricht" das Programm nicht, beeinflusst jedoch das Laufzeitverhalten erheblich. Ich habe gesehen, wie VLD ein Programm so verlangsamt hat, dass es unbrauchbar wurde.
Haliburton

Antworten:

40

Ja, das ist durchaus möglich. Ich bin mir sicher, dass es viele Möglichkeiten gibt, aber nehmen wir an, dass die Include-Datei eine globale Variablendefinition enthält, die als Konstruktor bezeichnet wird. Im ersten Fall würde der Konstruktor ausgeführt, im zweiten Fall nicht.

Das Einfügen einer globalen Variablendefinition in eine Header-Datei ist ein schlechter Stil, aber möglich.

John
quelle
1
<iostream>in der Standardbibliothek macht genau das; Wenn eine Übersetzungseinheit enthält, <iostream>wird das std::ios_base::Initstatische Objekt beim Programmstart erstellt, wobei die Zeichenströme std::coutusw. initialisiert werden , andernfalls nicht.
ecatmur
33

Ja das ist möglich

Alles, was #includes betrifft, geschieht zur Kompilierungszeit. Aber Kompilierungszeiten können das Verhalten zur Laufzeit natürlich ändern:

some/code.h::

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

dann

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

Mit der #includeÜberlastungsauflösung findet man das passender foo(int)und druckt daher 1statt 2. Da FOOes definiert ist, wird es zusätzlich gedruckt FOO.

Das sind nur zwei (nicht verwandte) Beispiele, die mir sofort in den Sinn kamen, und ich bin sicher, dass es noch viel mehr gibt.

pasbi
quelle
14

Um nur auf den trivialen Fall hinzuweisen: Precompiler-Direktiven:

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

Und dann

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

Es ist vielleicht pathologisch, aber ich habe einen ähnlichen Fall erlebt:

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

using namespace std;

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

Sieht harmlos aus. Versucht anzurufen std::max. Windows.h definiert jedoch max als

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

Wenn dies std::maxder Fall wäre, wäre dies ein normaler Funktionsaufruf, der f () einmal und g () einmal auswertet. Mit Windows.h wird f () oder g () jetzt zweimal ausgewertet: einmal während des Vergleichs und einmal, um den Rückgabewert zu erhalten. Wenn f () oder g () nicht idempotent war, kann dies zu Problemen führen. Zum Beispiel, wenn einer von ihnen ein Zähler ist, der jedes Mal eine andere Zahl zurückgibt ....

Cort Ammon
quelle
+1 für das Aufrufen der Max-Funktion von Window, ein reales Beispiel für die Implementierung einer bösen Implementierung und ein Fluch für die Portabilität überall.
Scott M
3
OTOH, wenn Sie es loswerden using namespace std;und verwenden std::max(f(),g());, wird der Compiler das Problem erkennen (mit einer undurchsichtigen Nachricht, die jedoch zumindest auf die Aufrufstelle verweist).
Ruslan
@ Ruslan Oh ja. Wenn Sie die Chance haben, ist das der beste Plan. Aber manchmal arbeitet man mit Legacy-Code ... (nein ... nicht bitter. Überhaupt nicht bitter!)
Cort Ammon
4

Möglicherweise fehlt eine Vorlagenspezialisierung.

// 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
quelle
4

Binäre Inkompatibilität, Zugriff auf ein Mitglied oder noch schlimmer, Aufruf einer Funktion der falschen Klasse:

#pragma once

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

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

#endif

Eine Funktion verwendet es und es ist in Ordnung:

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

Eine andere Version der Klasse einbringen:

#pragma once

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

class class_w
{
public: int a;
};

#endif

Bei Verwendung der Hauptfunktionen ändert die zweite Definition die Klassendefinition. Dies führt zu binärer Inkompatibilität und stürzt einfach zur Laufzeit ab. Beheben Sie das Problem, indem Sie das erste Include in main.cpp entfernen:

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

Keine der Varianten erzeugt einen Kompilierungs- oder Verbindungszeitfehler.

Die umgekehrte Situation, in der ein Include hinzugefügt wird, behebt den Absturz:

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

Diese Situationen sind noch viel schwieriger, wenn ein Fehler in einer alten Programmversion behoben oder eine externe Bibliothek / DLL / ein freigegebenes Objekt verwendet wird. Deshalb müssen manchmal die Regeln der binären Abwärtskompatibilität befolgt werden.

armagedescu
quelle
Der zweite Header wird aufgrund von ifndef nicht aufgenommen. Andernfalls wird es nicht kompiliert (eine Neudefinition der Klasse ist nicht zulässig).
Igor R.
@IgorR. Aufmerksam sein. Der zweite Header (include1.h) ist der einzige im ersten Quellcode enthaltene. Dies führt zu einer binären Inkompatibilität. Dies ist genau der Zweck des Codes, um zu veranschaulichen, wie ein Include zur Laufzeit zum Absturz führen kann.
armagedescu
1
@IgorR. Dies ist ein sehr vereinfachter Code, der eine solche Situation veranschaulicht. Aber im wirklichen Leben kann die Situation viel komplizierter sein. Versuchen Sie, ein Programm zu patchen, ohne das gesamte Paket neu zu installieren. Dies ist die typische Situation, in der die Regeln für die Abwärtsbinärkompatibilität strikt eingehalten werden müssen. Andernfalls ist das Patchen eine unmögliche Aufgabe.
armagedescu
Ich bin nicht sicher, was "der erste Quellcode" ist, aber wenn Sie meinen, dass 2 Übersetzungseinheiten 2 verschiedene Definitionen einer Klasse haben, handelt es sich um eine ODR-Verletzung, dh ein undefiniertes Verhalten.
Igor R.
1
Das ist undefiniertes Verhalten , wie im C ++ Standard beschrieben. FWIW ist es natürlich möglich, auf diese Weise eine UB zu verursachen ...
Igor R.
3

Ich möchte darauf hinweisen, dass das Problem auch in C besteht.

Sie können dem Compiler mitteilen, dass eine Funktion eine Aufrufkonvention verwendet. Wenn Sie dies nicht tun, muss der Compiler erraten, dass er den Standard verwendet, anders als in C ++, wo der Compiler die Kompilierung ablehnen kann.

Zum Beispiel,

main.c

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

foo.c

#include <stdio.h>

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

Unter Linux unter x86-64 ist meine Ausgabe

0

Wenn Sie den Prototyp hier weglassen, geht der Compiler davon aus, dass Sie ihn haben

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

Und die Konvention für nicht spezifizierte Argumentlisten verlangt, floatdass konvertiert wird, um übergeben doublezu werden. Obwohl ich gegeben habe 1.0f, konvertiert der Compiler es in 1.0d, um es an zu übergeben foo. Und gemäß AMD64 Architecture Processor Supplement für System V Application Binary Interface doublewird das in den 64 niedrigstwertigen Bits von übergeben xmm0. Aber fooerwartet einen Schwimmer, und es liest sie aus den 32 am wenigsten signifikanten Bits xmm0und bekommt 0.

izmw1cfg
quelle