おはようございます、高木です。

これまで、ラッパーライブラリの方針やクライアントコードのイメージについて検討を重ねてきました。
このあたりで、いったんこれまでに決めた内容をまとめてみることにします。

  • header-onlyライブラリとし、tcl.hhおよびtk.hhをインクルードするだけで使用できる。
  • 名前空間はcpp14tk::tclおよびcpp14tk::tkを使用し、cpp14tkはインライン名前空間とする。
  • ttkの名前空間はcpp14tk::tk::ttkとする。
  • Tcl/Tkは単一の専用スレッドの中だけで使用する。ただし、非同期イベントを他のスレッドから発生させることはあり得る。
  • カレントインタープリタを登録する方式を採用し、複数のインタープリタを複雑に組み合わせるような使い方は想定しない。


ざっとこんなところかと思います。

カレントインタープリタを設定することについては、前回の記事で書きました。
単一のスレッドでのみTcl/Tkを扱い、カレントインタープリタを設定するということは、素直に考えれば非局所オブジェクトを登場させることを意味します。

カレントインタープリタを複数のスレッドから触ることはないので、排他制御やvolatile修飾の類いは不要です。
また、heade-onlyですので、オブジェクトを外部結合にしなければなりません。
内部結合にしようとすると、翻訳単位ごとに別の実体ができてしまいます。

世間では「グローバル変数」というものが極端に嫌われる傾向にあるようです。
濫用すべきでないのは確かですが、問答無用で全面禁止というのはやり過ぎです。

そもそも、「グローバル変数」の定義をどうするかが曖昧だったりもします。
文字通りに解釈すれば、大域的名前空間有効範囲で宣言された外部結合を持つオブジェクトのことになるのでしょう。
しかし、何らかの名前空間内で宣言されればそれでいいのかというと、それでは「グローバル変数」が持つデメリットがほぼそのまま残ってしまいます。
では、何らかのクラスのメンバーにすればいいかというと、publicメンバであれば何らかの名前空間内で宣言された場合と変わらないでしょう。

アクセッサーを介してしか読み書きできないようにしてはどうでしょうか?
これなら、アクセッサーにブレークポイントを設定することで、実際に読み書きするタイミングを捉えることができます。
多少はましになったかもしれません。

「グロ-バル変数」がいつどこで変更されるかわからないから問題だとするなら、アクセッサーを使っても同じ問題は残り続けます。
ただし、必ずアクセッサーを介してしか変更できないのであれば、ポインターで持ち回られることだけは回避できます。

一般論として、翻訳単位をまたがる非局所オブジェクトの動的初期化は順序が不定になるという問題もあります。
Tcl_Interp*を含めたC互換型(POD型)では、こうした問題が生じることはないでしょう。

最後に、複数の翻訳単位からヘッダーファイルがインクルードされるとき、単一定義規則に抵触してエラーにならない工夫が必要です。

このように考えると、選択肢はいくつかに絞られてきます。

まずは、インライン関数の中で静的記憶域期間を持つオブジェクトとして定義し、その参照を返す方法が考えられます。
これであれば、単一定義規則の問題はクリアできますが、ポインタで持ち回られますし、グローバル変数のデメリットをすべて持っています。

次に、C++14から導入された変数テンプレートを使う方法が考えられます。
テンプレート引数のTypeとTagに適当な型を渡すことで使用できます。
今回であれば、TypeにはTcl_Interp*を、Tagは何でもいいのでvoidでも渡せばいいでしょう。
これなら単一定義規則の問題をクリアできるのはもちろん、POD型以外を適用しようとするとエラーにできます。
ただし、まるっきりのグローバル変数ですので、グローバル変数が持つデメリットをほぼすべて持っています。

テンプレートクラスを使って、アクセッサーを介してしか読み書きできないようにしたのが上のコードです。
アクセッサーはインライン関数であり、必要以上のことは一切していないので、オーバーヘッドもありません。
今回はstd::enable_if_tを使うとどうしてもコードが分かりにくくなるので、static_assertを使ってみました。

というわけで、今回は最後のテンプレートクラスを使う案を採用し、実質的には「グローバル変数」ですが、多少はマシなものにしました。