欠落している#includeが実行時にプログラムを破壊する可能性はありますか?


31

#includeビルドがまだ行われているときに、実行時にソフトウェアが壊れるケースはありますか?

つまり、

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

そして

complexLogic();
cleverAlgorithms();

両方とも正常にビルドされますが、動作は異なりますか?


1
おそらく、インクルードを使用すると、関数の実装で使用されるものとは異なる再定義された構造をコードに取り込むことができます。これにより、バイナリの非互換性が生じる可能性があります。このような状況は、コンパイラとリンカでは処理できません。
armagedescu

11
確かにそうです。ヘッダーが#includedの後に続くコードの意味を完全に変更するマクロをヘッダーに定義するのは非常に簡単です。
ピーター

4
Code Golfはこれに基づいて少なくとも1つの課題を達成したと確信しています。
マーク

6
具体的な実例として、メモリリーク検出用のVLDライブラリを指摘しておきます。VLDがアクティブな状態でプログラムが終了すると、一部の出力チャネルで検出されたすべてのメモリリークが出力されます。VLDライブラリにリンクし、#include <vld.h>コード内の1つの行を戦略的な位置に配置することで、プログラムに統合します。そのVLDヘッダーを削除または追加してもプログラムは「破壊」されませんが、ランタイムの動作に大きな影響を与えます。VLDがプログラムを使用できなくなるほど遅くなるのを見てきました。
ハリバートン

回答:


40

はい、それは完全に可能です。方法はたくさんあると思いますが、インクルードファイルにコンストラクターを呼び出すグローバル変数定義が含まれているとします。最初のケースではコンストラクターが実行され、2番目のケースでは実行されません。

ヘッダーファイルにグローバル変数の定義を置くのはスタイルが悪いですが、それは可能です。


1
<iostream>標準ライブラリでは正確にこれを行います。変換ユニットに含まれ<iostream>std::ios_base::Initいる場合、プログラムの開始時に静的オブジェクトが作成され、文字ストリームstd::coutなどが初期化されます。それ以外の場合は作成されません。
ecatmur

33

はい、可能です。

#includes に関するすべてはコンパイル時に発生します。しかし、コンパイル時に物事は実行時に動作を変更する可能性があります。もちろん:

some/code.h

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

その後

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

を使用すると#include、オーバーロードの解決がより適切foo(int)であり、したがっての1代わりに印刷され2ます。また、FOOが定義されているため、さらに印刷され FOOます。

それはすぐに頭に浮かんだ2つの(無関係の)例にすぎません。もっとたくさんあると思います。


14

ささいなケースを指摘するために、プリコンパイラ指令:

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

その後

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

それはおそらく病的ですが、私は関連するケースが発生しました:

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

using namespace std;

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

無害に見えます。の呼び出しを試みますstd::max。ただし、windows.hは最大値を

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

これがの場合、これはstd::max、f()とg()を1回評価する通常の関数呼び出しになります。しかし、windows.hがあると、f()またはg()が2回評価されます。1回は比較中に、もう1回は戻り値を取得します。f()またはg()がべき等でなかった場合、これは問題を引き起こす可能性があります。たとえば、そのうちの1つがたまたまカウンターであり、毎回異なる数値を返す場合などです。


+1は、Windowのmax関数を呼び出すための実装例です。実装の悪さや、あらゆる場所への移植性への悩みの実例です。
スコットM

3
OTOH、を削除してusing namespace std;使用するstd::max(f(),g());と、コンパイラーが問題をキャッチします(あいまいなメッセージが表示されますが、少なくとも呼び出しサイトを指します)。
ルスラン

@ルスランああ、そうです。機会が与えられれば、それは最良の計画です。しかし、時々、1つはレガシーコードで作業しています...(いいえ...苦くない。まったく苦くない!)
Cort Ammon

4

テンプレートの特殊化が欠落している可能性があります。

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

バイナリの非互換性、メンバーへのアクセス、またはさらに悪いことに、間違ったクラスの関数を呼び出す:

#pragma once

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

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

#endif

関数がそれを使用しており、問題ありません。

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

クラスの別のバージョンを取り込む:

#pragma once

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

class class_w
{
public: int a;
};

#endif

mainの関数を使用して、2番目の定義はクラス定義を変更します。バイナリの非互換性が発生し、実行時にクラッシュするだけです。そして、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;
}

バリアントのいずれも、コンパイル時またはリンク時のエラーを生成しません。

逆の場合、インクルードを追加するとクラッシュが修正されます。

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

古いバージョンのプログラムのバグを修正したり、外部のライブラリ/ dll /共有オブジェクトを使用したりすると、これらの状況はさらに困難になります。そのため、バイナリ下位互換性のルールに従う必要がある場合があります。


ifndefのため、2番目のヘッダーは含まれません。それ以外の場合はコンパイルされません(クラスの再定義は許可されていません)。
イゴールR.

@IgorR。行き届きます。2番目のヘッダー(include1.h)は、最初のソースコードに含まれる唯一のものです。これにより、バイナリの非互換性が発生します。これがまさに、インクルードが実行時にクラッシュを引き起こす可能性があることを示すためのコードの目的です。
armagedescu

1
@IgorR。これは非常に単純なコードで、このような状況を示しています。しかし、実際の状況では、はるかに複雑なサブタイルになる可能性があります。パッケージ全体を再インストールせずに、いくつかのプログラムにパッチを適用してみてください。これは、後方バイナリ互換性規則に厳密に従う必要がある典型的な状況です。それ以外の場合、パッチの適用は不可能です。
armagedescu

「最初のソースコード」が何であるかはわかりませんが、2つの翻訳単位にクラスの2つの異なる定義があることを意味する場合、それはODR違反、つまり未定義の動作です。
イゴールR.

1
C ++標準で説明されているように、これは未定義の動作です。FWIW、もちろん、この方法でUBを引き起こすことは可能です...
イゴールR.

3

問題はCにも存在することを指摘しておきます。

関数が呼び出し規約を使用していることをコンパイラに伝えることができます。そうしないと、コンパイラーはコンパイルを拒否できるC ++とは異なり、コンパイラーはデフォルトのものを使用することを推測する必要があります。

例えば、

main.c

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

foo.c

#include <stdio.h>

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

Linux on x86-64では、私の出力は

0

ここでプロトタイプを省略すると、コンパイラーは、

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

また、未指定の引数リストの規約では、それfloatdouble渡すために変換する必要があります。だから私は与えた1.0fが、コンパイラはそれを1.0dに変換してに渡しfooます。また、System VアプリケーションバイナリインターフェイスAMD64アーキテクチャプロセッササプリメントによればdouble、は64の最下位ビットで渡されますxmm0。しかしfoo、floatを期待し、それを32の最下位ビットから読み取りxmm0、0を取得します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.