È possibile che un #include mancante rompa il programma in fase di esecuzione?


31

Esiste un caso in cui mancare a #includeromperebbe il software in fase di esecuzione, mentre la compilazione continua?

In altre parole, è possibile che

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

e

complexLogic();
cleverAlgorithms();

costruiranno entrambi con successo, ma si comporteranno diversamente?


1
Probabilmente con i tuoi include potresti portare nel tuo codice strutture ridefinite che sono diverse da quelle usate dall'implementazione delle funzioni. Ciò può comportare incompatibilità binaria. Tali situazioni non possono essere gestite dal compilatore e dal linker.
Armagedescu,

11
Certamente è. È abbastanza facile avere macro definite in un'intestazione che cambiano completamente il significato del codice che viene dopo che l'intestazione è #included.
Peter,

4
Sono sicuro che Code Golf ha fatto almeno una sfida in base a questo.
Segna il

6
Vorrei sottolineare un esempio specifico del mondo reale: la libreria VLD per il rilevamento di perdite di memoria. Quando un programma termina con VLD attivo, stamperà tutte le perdite di memoria rilevate su alcuni canali di uscita. Lo si integra in un programma collegandosi alla libreria VLD e posizionando una singola riga #include <vld.h>in una posizione strategica nel codice. La rimozione o l'aggiunta dell'intestazione VLD non "interrompe" il programma, ma influisce in modo significativo sul comportamento di runtime. Ho visto VLD rallentare un programma al punto da renderlo inutilizzabile.
Haliburton,

Risposte:


40

Sì, è perfettamente possibile. Sono sicuro che ci sono molti modi, ma supponiamo che il file include contenga una definizione di variabile globale che ha chiamato un costruttore. Nel primo caso il costruttore avrebbe eseguito, e nel secondo no.

Inserire una definizione di variabile globale in un file di intestazione è uno stile scadente, ma è possibile.


1
<iostream>nella libreria standard fa esattamente questo; se qualsiasi unità di traduzione include, <iostream>l' std::ios_base::Initoggetto statico verrà costruito all'avvio del programma, inizializzando i flussi di caratteri std::cout, ecc., altrimenti no.
ecatmur

33

Sì, è possibile.

Tutto ciò che riguarda #includes accade al momento della compilazione. Ma in fase di compilazione le cose possono cambiare il comportamento in fase di esecuzione, ovviamente:

some/code.h:

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

poi

#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 #include, la risoluzione del sovraccarico trova la soluzione più appropriata foo(int)e quindi stampa 1anziché 2. Inoltre, poiché FOOè definito, stampa ulteriormente FOO.

Sono solo due esempi (non correlati) che mi sono venuti subito in mente, e sono sicuro che ce ne sono molti altri.


14

Giusto per sottolineare il banale caso, le direttive del precompilatore:

// 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 poi

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

È patologico, forse, ma ho avuto un caso correlato:

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

using namespace std;

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

Sembra innocuo. Prova a chiamare std::max. Tuttavia, windows.h definisce max

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

In questo caso std::max, si tratterebbe di una normale chiamata di funzione che valuta f () una volta e g () una volta. Ma con windows.h all'interno, ora valuta due volte f () o g (): una volta durante il confronto e una volta per ottenere il valore restituito. Se f () o g () non era idempotente, ciò può causare problemi. Ad esempio, se uno di questi è un contatore che restituisce un numero diverso ogni volta ....


+1 per richiamare la funzione massima di Window, un esempio reale di inclusione malvagia dell'implementazione e una rovina per la portabilità ovunque.
Scott M

3
OTOH, se ti sbarazzi di using namespace std;e usi std::max(f(),g());, il compilatore afferrerà il problema (con un messaggio oscuro, ma almeno indicando il sito di chiamata).
Ruslan,

@Ruslan Oh, sì. Se gli viene data la possibilità, è il piano migliore. Ma a volte si sta lavorando con il codice legacy ... (no ... non è amaro. Non è affatto amaro!)
Cort Ammon

4

È possibile che manchi una specializzazione del modello.

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

Incompatibilità binaria, accesso a un membro o, peggio ancora, chiamata una funzione della classe sbagliata:

#pragma once

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

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

#endif

Una funzione la usa ed è ok:

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

Inserisci un'altra versione della classe:

#pragma once

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

class class_w
{
public: int a;
};

#endif

Usando le funzioni in main, la seconda definizione cambia la definizione di classe. Porta a incompatibilità binaria e si arresta semplicemente in fase di esecuzione. E risolvi il problema rimuovendo la prima inclusione in 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;
}

Nessuna delle varianti genera un errore di tempo di compilazione o collegamento.

La situazione viceversa, l'aggiunta di un'inclusione risolve l'arresto anomalo:

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

Queste situazioni sono ancora più difficili quando si correggono i bug in una vecchia versione del programma o si utilizza un oggetto library / dll / shared esterno. Ecco perché a volte devono essere seguite le regole della compatibilità binaria con le versioni precedenti.


La seconda intestazione non verrà inclusa a causa di ifndef. Altrimenti non verrà compilato (la ridefinizione della classe non è consentita).
Igor R.

@IgorR. Sii attento. La seconda intestazione (include1.h) è l'unica inclusa nel primo codice sorgente. Questo porta all'incompatibilità binaria. Questo è esattamente lo scopo del codice, per illustrare come un'inclusione può portare a un arresto anomalo in fase di esecuzione.
Armagedescu,

1
@IgorR. questo è un codice molto semplicistico, che illustra tale situazione. Ma nella vita reale la situazione può essere molto più complicata. Prova a correggere alcuni programmi senza reinstallare l'intero pacchetto. È la situazione tipica in cui devono essere seguite rigorosamente le regole di compatibilità binaria all'indietro. Altrimenti l'applicazione di patch è un compito impossibile.
Armagedescu,

Non sono sicuro di quale sia il "primo codice sorgente", ma se intendi che 2 unità di traduzione hanno 2 diverse definizioni di una classe, si tratta di violazione ODR, ovvero comportamento indefinito.
Igor R.

1
Questo è un comportamento indefinito , come descritto dallo standard C ++. FWIW, ovviamente, è possibile causare un UB in questo modo ...
Igor R.

3

Voglio sottolineare che il problema esiste anche in C.

Puoi dire al compilatore che una funzione usa una convenzione di chiamata. In caso contrario, il compilatore dovrà indovinare che utilizza quello predefinito, a differenza di C ++ in cui il compilatore può rifiutarsi di compilarlo.

Per esempio,

main.c

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

foo.c

#include <stdio.h>

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

Su Linux su x86-64, il mio output è

0

Se ometti il ​​prototipo qui, il compilatore presuppone che tu abbia

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

E la convenzione per elenchi di argomenti non specificati richiede che floatdebba essere convertito doubleper essere passato. Quindi, anche se ho dato 1.0f, il compilatore lo converte in 1.0dper passarlo foo. E secondo il supplemento al processore di architettura AMD64 per l'interfaccia binaria dell'applicazione System V, i passaggi doublevengono passati nei 64 bit meno significativi di xmm0. Mafoo aspetta un float, lo legge dai 32 bit meno significativi di xmm0e ottiene 0.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.