
前提
現在C++のデバッグ用の(ヘッダーオンリー)ライブラリを趣味で(OSSとして)作っていてます。
そのライブラリの設定コードを今までmain関数内に書くようにしていたのですが、出来ることなら開発環境かそうでないかで#include
するヘッダを分岐することで実現したい、つまりmain関数外に処理を書けないかと思っていました。
実現イメージ
今までこう書いていたものを、
cpp
1// main.cpp内 ------------------------------------------------------ 2#include "my_library.hpp" 3 4int main() { 5#ifdef DEBUGGING 6 // ここにライブラリの設定コードを書く 7 // なお、設定コードはヘッダーで定義されたグローバルなinline変数に値を代入するというもの 8 my_library::option = value; 9#endif 10 11 // 以下main()のコードが続く... 12}
こう書きたいです。
cpp
1// debug.hpp: デバッグ時に読み込むヘッダファイルかソースファイル --------- 2#include "my_library.hpp" 3 4// ここにライブラリの設定コードを書く 5my_library::option = value; 6 7 8// 以下main.cpp内 --------------------------------------------------- 9#ifdef DEBUGGING 10#include "debug.hpp" 11#endif 12 13int main() { 14 // 以下main()のコードが続く... 15}
実現方法案: コンストラクタを使う
main関数前の処理の実行を実現するために、以下の方法を考えました。
cpp
1// ライブラリ側で用意する ---------------------------------------------- 2// ※namespaceは省略します 3struct execute_before_main { 4 template<typename Func> 5 execute_before_main(Func func) { 6 func(); 7 } 8 static execute_before_main perform; 9}; 10 11// ユーザーがdebug.hppに書く処理 --------------------------------------- 12// ソースファイルに書く場合はinlineは不要 13inline execute_before_main execute_before_main::perform([]{ 14 // ここにライブラリの設定コードを書く 15 my_library::option = value; 16});
質問
この方法をあまり見たことがないのですが、この方法にはバグになりうるような部分がありますでしょうか?
もちろん、変なことをせずにmain関数内に書いた方がいいとは思いますが、あえてmain関数外にも書けるようにこの方法も用意しておく場合、どんなバグが考えられるでしょうか?
なお、実際に実装してみたところ、Clang、GCC、MSVCではきちんと動きました。
補足1: 環境について
C++17以上を想定しています。
補足2: 考えた方法に関して、バグになりうる部分がないか、自問自答
以下にバグになりうりそうな要因を自問自答してみた内容を書きます。
この考えがあっているか、もしくはこれ以外にもないか教えてくださると幸いです。
1. グローバル変数の初期化の実行順によるバグがありそう
今回の場合、コンストラクタで処理を実行するために(static)変数を定義しています。
その処理の中で、コンストラクタがまだ実行されていないグローバル変数を参照したり書き換えてしまうとバグが発生するというものです。
しかし、設定コードとして書くものは基本的にboolやenum型のグローバルなinline変数に代入するというもので、他のクラスのメンバを書き換えるみたいなことはしないので大丈夫だと思っています。
また、グローバル変数は定義順に初期化が実行されるという観点でも、ヘッダーオンリーライブラリだから、必ず定義(初期化)が先に来て、その後にexecute_before_main
のコンストラクタが呼ばれるようになっています。
また、ユーザーにもこの問題のことは伝えておいて、ライブラリの設定コード以外は書かないように注意しておきます。
2. コンストラクタで処理を実行するためだけに参照されない変数を新たに定義するのは名前空間が汚れる
ユーザーに新しく名前空間を作らせてもよいですが、execute_before_main
のstaticメンバの定義とすると名前空間を作らずに変数をネスト出来て、手軽でよいかなと思いました。
3. このライブラリは設定コードを書かなくても使用できるようにしたいが、その場合execute_before_main
のstaticメンバ(perform)は宣言のみで、定義がなくなってしまうことについて
performはODR-usedされていないので、定義がなくてもコンパイラはNDR(診断不要)。でも、NDRはコンパイラはエラーをはかないけど、未定義動作を引き起こす可能性は否定していないから避けた方がいいか?でもODR-usedされていない変数が定義されてないだけだから、実質未定義動作は起こらない気がするけど、、
もしダメな場合はstaticメンバは削除してグローバル変数を定義してもらう方針に変える。
追記: 後で思ったのですが、execute_before_main
をテンプレートにして、実体化を遅らせればいいのではと思いました。つまり以下のような形です。(これで実際に動きました)
cpp
1template<typename = void> 2struct execute_before_main { 3 template<typename Func> 4 execute_before_main(Func func) { 5 func(); 6 } 7 static execute_before_main perform; 8}; 9 10// perform変数の定義。それと同時にテンプレート引数に 11// voidを取った時のexecute_before_mainのテンプレート実体化 12// 文法の解釈が難しいですが、テンプレートの実体化をしているとだけ思ってほしいです 13// ソースファイルに書く場合はinlineは不要 14template<> 15inline execute_before_main<> execute_before_main<>::perform([]{ 16 // ここにライブラリの設定コードを書く 17});
4. 使用されないstaticメンバ変数の定義文として書いているので、最適化などでコンパイラに削除されてしまわないか
これに関しては実際に試してみましたが、手元の環境のclang++とg++のO2、O3、リンク時最適化、MSVCのO1、O2の最適化では削除されることはありませんでした。
補足3: コメントを受けて
設定コード用の変数の初期化自体はライブラリで行っています。
その値をカスタマイズしたいという場合に、main関数外で値を変えられたらなと思っています。
また、オプションの内容はユーザーの好みレベルのもので、プロジェクト全体を通して変わらない定数と考えてもらって構いません。
cpp
1// ライブラリ内 2namespace my_library { 3// このライブラリがログを出力するときの1行当たりの文字数のデフォルト値 4inline int max_line_width = 80; 5} 6 7// ユーザーコード内 8// ソースファイルに書く場合はinlineは不要 9inline execute_before_main execute_before_main::perform([]{ 10 // このライブラリがログを出力するときの1行当たりの文字数を100に変更 11 my_library::max_line_width = 100; 12});





回答2件
あなたの回答
tips
プレビュー