CMakeでC ++プリプロセッサ定義を正しく配置する

プリプロセッサ定義は、プラットフォーム固有のコードなど、選択したコードのセクションを条件付きでコンパイルするために、C ++プロジェクトでよく使用されます。 この記事では、明らかに、コンパイラのフラグを介して#defineを付加するときに踏むことができる唯一の(しかしデバッグが非常に難しい)レーキが考慮されます。



例として、 CMakeビルドシステムを取り上げますが、他の一般的な同等物でも同じアクションを実行できます。



はじめにと問題の説明



Windows / Linuxでのチェックなど、一部のプラットフォーム固有の定義はコンパイラーによって書き込まれるため、ビルドシステムの追加の支援なしで使用できます。 ただし、#インクルードファイルの存在、プリミティブ型の存在、システム内の必要なライブラリの存在、またはシステムのビット数の単純な決定など、他の多くのチェックは「外部」で実行する方がはるかに簡単であり、必要な定義をコンパイラフラグに渡します。 さらに、追加の定義を単に渡すことができます。



#defineコンパイラフラグを渡す例
g++ myfile.cpp -D MYLIB_FOUND -D IOS_MIN_VERSION=6.1
      
      





 #ifdef MYLIB_FOUND #include <mylib/mylib.h> void DoStuff() { mylib::DoStuff(); } #else void DoStuff() { // own implementation } #endif
      
      







CMakeでは、コンパイラに#definesを追加するにはadd_definitionsを使用します 。これにより、ほとんどすべてのCMakeコマンドと同様に、現在のプロジェクト全体とそのサブプロジェクトにコンパイラフラグが追加されます。



 add_definitions(-DMYLIB_FOUND -DIOS_MIN_VERSION=6.1)
      
      





ここでは問題ないように思えます。 ただし、不注意で重大な間違いを犯す可能性があります。



同じプロジェクトAのヘッダーファイルでプロジェクトAのコンパイラーによって配置された#defineがチェックされている場合、サブプロジェクトAではない別のプロジェクトBからこのヘッダーファイルを#includeすると、この#defineは配置されません



例1(シンプル)


説明されているエラーの実例はgithub / add_definitions / wrongにあります。 スポイラーの下では、念のため、重要なコードが重複しています。



add_definitions /間違っている
 project(wrong) add_subdirectory(lib) add_subdirectory(exe)
      
      





 project(lib) add_definitions(-DMYFLAG=1) add_library(lib lib.h lib.cpp)
      
      





 project(exe) add_executable(exe exe.cpp) target_link_libraries(exe lib)
      
      







 // lib.h static void foo() { #ifdef MYFLAG std::cout << "foo: all good!" << std::endl; #else std::cout << "foo: you're screwed :(" << std::endl; #endif } void bar(); // implementation in lib.cpp
      
      





 // lib.cpp #include "lib.h" void bar() { #ifdef MYFLAG std::cout << "bar: all good!" << std::endl; #else std::cout << "bar: you're screwed :(" << std::endl; #endif }
      
      





 // exe.cpp #include "lib/lib.h" int main() { foo(); bar(); }
      
      







exeを実行すると出力されます:



 foo: you're screwed :( bar: all good!
      
      





この例は非常に簡単です。コンソールへの出力もあります。 実際には、 Intel Threading Building Blocksのような非常に高度なライブラリを接続すると、このようなエラーが発生する可能性があります。ここでは、低レベルパラメータの一部がプリプロセッサ定義を通じて実際に渡され、ヘッダーファイルでも使用されます。 このような条件下で驚くべきエラーを見つけることは、特にこのadd_definitionsのニュアンスがこれまでに出会ったことがない場合、非常に苦痛で長くかかります。



例2


明確にするために、2つのプロジェクトの代わりに1つを使用し、add_definitionsの代わりにコード内に通常の#defineがあり、CMakeを完全に放棄します。 この例は、非常に単純化されていますが、実際の状況であり、C ++の一般的な知識の観点からも興味深いものです。



実行中のコードはgithub / add_definitions / cpphellで確認できます。 前の例のように、スポイラーの下のコードの重要なセクション:



add_definitions / cpphell
 // ah class A { public: A(); // implementation in a.cpp with DANGER defined ~A(); // for illustrational purposes #ifdef DANGER std::vector<int> just_a_vector_; std::string just_a_string_; #endif // DANGER };
      
      





 // a.cpp #define DANGER // let's have a situation #include "ah" A::A() { std::cout << "sizeof(A) in A constructor = " << sizeof(A) << std::endl; } A::~A() { std::cout << "sizeof(A) in A destructor = " << sizeof(A) << std::endl; std::cout << "Segmentation fault incoming..." << std::endl; }
      
      





 // main.cpp #include "ah" // DANGER will not be defined from here void just_segfault() { A a; // segmentation fault on 'a' destructor } void verbose_segfault() { A *a = new A(); delete a; } int main(int argc, char **argv) { std::cout << "sizeof(A) in main.cpp = " << sizeof(A) << std::endl; // verbose_segfault(); // uncomment this just_segfault(); std::cout << "This line won't be printed" << std::endl; }
      
      







間違いは美しいです。 一方のファイル(a.cpp)は、#ifdefの下に隠されたクラスのメンバーを参照し、もう一方(main.cpp)は参照しません。 これらのクラスでは、クラスのサイズが異なるため、メモリ管理、特にセグメンテーションフォールトの問題が発生します。



 g++ main.cpp a.cpp -o main.out && ./main.out
      
      





 sizeof(A) in main.cpp = 1 sizeof(A) in A constructor = 32 sizeof(A) in A destructor = 32 Segmentation fault incoming... Segmentation fault (core dumped)
      
      





main.cppのverbose_segfault()の呼び出しのコメントを外すと、最後に表示されます:



 *** Error in `./main.out': free(): invalid next size (fast): 0x000000000149f010 *** ======= Backtrace: ========= ... ======= Memory map: ======== ...
      
      





一定数の実験の後、クラスAのフィールドでSTLクラスの代わりに任意の数のプリミティブ型を使用する場合、デストラクタが呼び出されないため、ドロップはありませんでした。 さらに、孤立したstd :: string(64ビットArch LinuxおよびGCC 4.9.2のsizeof(std :: string)== 8)を挿入した場合、ドロップはなく、2つある場合は既に存在します。 私はポイントが整合であると思いますが、コメントで彼らが実際に何が起こっているかを詳細に説明できることを望みます。



可能な解決策



ヘッダーファイルで「外部」定義を使用しないでください


可能であれば、これが最も簡単なオプションです。 残念ながら、#ifdefsの下には、さまざまなプラットフォームおよびコンパイラ依存の関数シグネチャが存在することがあり、一部のライブラリは一般にヘッダーファイルのみで構成されています。



ルートCMakeLists.txtでadd_definitionsを使用します


これは、もちろん、特定のプロジェクトの「忘れられた」合格フラグの問題を解決しますが、結果は次のとおりです。





構成ヘッダーファイルとconfigure_fileを使用する


CMakeは、configure_fileを使用して構成ヘッダーファイルを作成する機能を提供します 。 リポジトリには、CMakeによるプロジェクトのビルド時に構成ファイル自体が生成される、事前に準備されたテンプレートが格納されます。 生成されたファイルは、必要なプロジェクトヘッダーファイルに#includedされます。



configure_fileを使用する場合、add_definitionsを使用して特定のプロジェクトの「外部」にプリプロセッサ定義を配置しても機能しないことに注意してください。 もちろん、フラグが設定されていない場合にのみフラグを置く特別な構成ファイルを作成できます(#ifndef)が、これによりさらに混乱が生じます。



おわりに



もちろん、表示されるエラーとソリューションオプションは、CMakeプロジェクトだけでなく、他のビルドシステムを使用するプロジェクトにも適しています。

ヘッダーファイルにプリプロセッサ定義があるC ++プロジェクトで完全に魔法のエラーをデバッグするときに、この記事がいつか誰かの時間を節約することを願っています。



All Articles