template クラスを private メンバーに含むクラスを DLL 化した時に出る C4251 warning について.

template クラスを private メンバーに含むクラスを DLL 化した際に「class ‘XXXXXX’ は __export キーワードを使って class ‘YYYYYY’ にエクスポートしてください」のような warning に出くわした方は多いと思います.

何故、template の export をしろと言われてしまうのか… 公式のドキュメントにもその理由についてははっきり書かれてはいないのであくまで推測になってしまいますが、このケース(正確には template メンバーが private、 inline での使用なし、メンバー関数の入出力に関係なし)に限ってはMSVC の処理系の都合で warning が出ていると思われます.

そもそも template のインスタンスの export が必要なのは「template のコードは翻訳単位ごとにインスタンス化される」と言う処理系の template サポートの方法と、実行時までどのようなコードがリンクされるか分からないと言う DLL の性質によるものと思われます. (MSVCがどのような実装になっているかは定かではないですが、恐らく例外ではないかと…  詳細は本の虫を参照)

仮に export を行わなかった場合、DLL 側が保持している template のインスタンスとクライアント側の template のインスタンスがシンボル的には一致しているにも関わらず処理内容が異なると言う事がありえますし、実際にそのような仕様になっているかは別として、DLL とクライアント側でコンパイルに使用された処理系のバージョンやオプションが異なる場合、元となった template の記述が同一であったとしても想定しているデータアライメント等が異なると言うことも起こり得ます.

一方クラスの template メンバー変数が private で inline でも使用されておらず、メンバー関数の入出力とも無縁な場合、基本的にはクライアント側でコードのインスタンス化は必要ないはずですが実際には C4251 warning が出てしまいます…   恐らく処理系の都合なのでしょうが、なぜこういったケースを検出できないのか??  template の仕様を逆手に取った private メンバーへのアクセス等まで考慮した結果なのか???

前置きが長くなってしまいました.

さて、この warning への対処方法ですが、メッセージ通りに template class のインスタンスを DLL から export する方法と合わせて代表的には以下の 3 つがあるのではないかと.

(1) #pragma で C4251 を無視する.
(2) メンバー変数として保持している template の instance を全て export する.
(3) pimpl イディオムを利用する.

(1) #pragma で C4251 を無視する.

もっとも楽な方法ですが、C4251 で警告されるのはこの template 絡みの warning のみではないようなので、DLL の危険性に関する知識がない場合 data crruption に繋がる warning を見逃してしまうかもしれません.  そうでなくても #pragma による warning 抑制はその意味と危険性を理解した上で行う最終手段的な側面があるため、むやみに使用せず他の手がないかまず考えるべきだと思います.

https://docs.microsoft.com/ja-jp/cpp/error-messages/compiler-warnings/compiler-warning-level-1-c4251

(2) メンバー変数として保持している template の instance を全て export

__declspec(dllexport) を使用してメンバー変数として保持している template のインスタンスを export する方法です.  自前で定義した単純な template の場合は問題ありませんが、STL のような複雑なものを export する場合、デフォルト指定されている template 変数 (STL コンテナのアロケーター等)や基底クラスまで遡って全てインスタンス化を行う必要があるため、中々な手間になる事があります. また std::map, std::set, std::queue, std::list, std::deque などはそもそも export 出来ないようなので注意が必要です.

https://support.microsoft.com/en-us/help/168958/how-to-export-an-instantiation-of-a-standard-template-library-stl-clas

(3) pimpl イディオムを利用する.

場合によってはアンチパターンと言われる事もありますが、pimpl をイディオムを使う事でもこの問題を回避出来ます.  デメリットはコードの記述量が増えてしまう点.

どうでしょう?  (1)(2) のに比べて危険なコードを書いてしまう可能性も低く、難しい事を考えなくても大丈夫かつ一番汎用性があるので、個人的には(3) を選ぶ事が多い気がしますが…

 

 

 

 

 

 

 

 

広告

const でない空の std::string に対する先頭要素への添え字 (operator[])アクセスは C++11 以前は未定義

ちょっと作業をしていて、最近知った仕様.  Fix した規格書ではなく working draft からの参照になってしまいますが、多分内容は変わっていないはず.

C++ Working Draft – n1905
Returns: If pos < size(), returns *(begin() + pos ). Otherwise, if pos == size(), the const version returns charT(). Otherwise, the behavior is undefined.

C++ Working Draft – n4659
Returns: *(begin() + pos) if pos < size(). Otherwise, returns a reference to an object of type charT with value charT(), where modifying the object to any value other than charT() leads to undefined behavior.

「if pos == size() の時」と言う事で直接的にそう書いてある訳ではないですが、空の std::string の場合は size() == 0 なので結果的に str[0] は未定義になります.

古い処理系を使用していて、本当は未定義だけど気を効かせてくれて non const でも問題なしとなっている場合はあえて目くじら立てる事はないのかもしれないですが…

(2018年現在においても C++11 以前の別の処理系に乗り換える可能性があるようなケースでは別.  STL を使用しているケースは多くないと思うけど、Embedded 系は稀に良くある??)

にしても、勉強不足を痛感…

 

GNU Make を単体テストのテストランナーに使用しようとして困ったことあれこれ

make test で単体テストが実行されるようになっているプロジェクトを CI/CD に対応させる際に色々と右往左往してしまったので make マスターの面々からするとヤレヤレな内容かもしれませんが… メモがてら.

ビルドシステムに make が採用されている場合、多くの場合サブディレクトリ毎に makefile が用意されていて、それをプロジェクトのトップディレクトリに置かれた makefile から再帰 make するというのがよく行われると思います.  例えば以下のような構成のプロジェクトでは (a) から (b), (c) の makefile を再帰 make すると言った具合です.

├── Makefile          — (a)
├── subdir0
│      └── Makefile  — (b)
└── subdir1
│      └── Makefile  — (c)

また、(b) (c) の makefile には疑似 ターゲットとして test が定義されており、各々のディレクトリで make test を打つことで lib の生成や単体テストのビルドと実行が行われるようになっています.  このプロジェクトを CI/CD 対応しようとしたとき、ツール側から沢山あるサブディレクトリを一つ一つ個別に叩いて行くのは面倒なので当然 (a) に定義された test ターゲットを叩いて済ませたくなる訳ですが、単体テストの実行には一般的に以下の要求事項があり、この要求を満足する形でテストランナーを実装しなくてはなりません.

(1) 途中で fail しても後続のテストは実行したい.
(2) 全てのテストを実行し終えた後に一つでも fail していた場合に全体の終了ステータスをエラーにしたい.

さてどうしたものかと言うことで、真っ先に思いついたのが for でサブディレクトリを回す方法ですが、この方法では呼び出し元の make にエラーが帰らないため、(2) の条件を満たす事ができません.

次に思いついたのが、以下のように test ターゲットのコマンド行に再帰 make を直書きして -k (–keep-going)オプションで make を起動するという方法でしたが、-k オプションはターゲットのコマンド行で発生したエラーを無視して続行するのではなく、エラーを起こしたターゲットの構築を中断して、別のターゲットの構築を続けて実行するというオプションのようなので期待した動作をしません.  (というのをやってみて気づいた)

Normally make gives up immediately in this circumstance, returning a nonzero status. However, if the ‘-k’ or ‘–keep-going’ flag is specified, make continues to consider the other prerequisites of the pending targets, remaking them if necessary, before it gives up and returns nonzero status. For example, after an error in compiling one object file, ‘make -k’ will continue compiling other object files even though it already knows that linking them will be impossible.

https://www.gnu.org/software/make/manual/html_node/Errors.html

そうこうして最終的に「サブディレクトリ毎に test_subdir0, test_subdir1 のようなテスト専用ターゲットを (a) に用意して -k オプションを付けて叩くしかない」という結論に至ったのですが、実際はディレクトリの数も多く、ベタ書きはダサいなー嫌だなーと言うことで無い知恵絞ってヒネリだしたのが以下

なんかとりとめもなくなってしまった&もっといい方法がありそうな気がしてならなですが、とりあえず要件は満たせた!??

「コードの綺麗さ」と言う言葉に対する表面的な理解

「コードの綺麗さ」といった場合、いくつか評価軸がありますが恐らくほぼ以下の 3 つに集約できるのではないかと思います.

(1) 関数内部、ファイル内部等の局所的に記述されているコードの見た目 (改行とかスペースとか) が綺麗かどうか
(2) 関数内部、ファイル内部等の局所的に記述されている処理や記述に無駄なく美しいかどうか
(3) モジュールの切り分け、命名等を含む、大域的に見た設計が統一され美しいかどうか

もちろん全部綺麗なことに越した事はないですが、(3)が最も重要度と抽象度が高く、逆に(1)は個人的にそこまで重要では無いと思っています.

何故なら、見た目の統一感の無さよりアーキテクチャ上の統一感の無さの方が余程問題だから. どうしても見た目を揃えたいならツールにやらせれば良いことで、このような作業を人間が時間を割いてやるほどアホで愚かな事はありません.  (後れ馳せながら最近仕事で golang を触り始めたのですが、go fmt を処理系と一緒に用意して宗教戦争も含めた無駄を排除するという考え方、すごく良いなと思いました.)

ですが、現実にはコードの綺麗さを(1)でしか測れず、コードレビューの際にもこういった見た目の指摘ばかり繰り返す人間がかなりの割合でいます.  そういう人達に欠如しているのは、本質が何なのかを理解する能力や、コードを俯瞰してみる(つまり木ではなく森を見る)と言う視点です.

優れたソフトウェアほど一貫した設計思想があり、その思想を理解することで細部を読まずとも全体を想像し、構造を把握する事が出来ます.

自分は何度か転職をしていますが、一番最初と現在の会社を除いてこのような視点を持っている人間は皆無でした.

以前自チームで構築したプログラムを同僚に解説せよと上から仰せつかった際、全体の規模が大きかった事もありアーキテクチャーの解説を中心に行った所、「お前の説明はなっていない」と言わんばかりの感じで何故それが知りたいのか理解出来ない細部の説明ばかりを求められた事があります.

転職の際の引き継ぎだったので無表情で言われるがまま応じましたが、言うまでもなく死ぬほどウンザリしました.

残念な組織ほどこういった本質に対する理解力の欠如した人間が大きな顔をし、挙句の果てにコーディング規約なんか作っちゃってるからもう…

C++ で Read-Write lock パターン

同じデーターに対して複数のスレッドが入り乱れて読み書きを行う場合、排他のオーバーヘッドが無視できずに問題になることがあります.

そこで Read アクセスのみの場合は排他を行わなわず、Read-Write がかち合った場合にのみ排他を行うことでオーバーヘッドの低減を図ろうというのがこのパターンです.

Read-Write lock パターンは shared mutex という形で標準ライブラリで提供されています (shared mutex の詳細については以下のリンクを参照下さい)

cppreference

C++ でマルチスレッドデザインパターン

C++ で Thread-Specific Storage パターン

複数のスレッドから読み書きされるデータはその内容の整合性を保つため、基本的には排他処理を行う必要があります(Single Thread Execution パターン

Thread-Specific Storage パターンは同じデータに複数スレッドからアクセスが行われ、かつスレッド間でのデータの共有が不要といったケースにおいて、スレッド毎に専用の領域を用意する事で排他を行わずに並列実行可能なようにすると言うパターンです.

C++11 から、新たな記憶クラス指定子として thread_local というものが用意されており、これを使うと簡単に Thread-Specific Storage パターンを使うことができます (thread_local 記憶クラス指定子についての詳細は以下のリンクを参照下さい)

cppreference

このパターンの実際の使い所ですが、積極的に使用するケースというのはあまりなく、「シングルスレッドでの使用を前提に静的領域を多用している(場合によってはレガシーな)コードを比較的簡単にマルチスレッド化する」といったような用途で使用される事が多いような気がします.

C++ でマルチスレッドデザインパターン

MacOS Sierra 10.12 + Xcode 8 + Qt 5.9.1 で QMake 時に「could not resolve SDK Path for ‘macosx’ 」 エラーになる問題

転職直後の大変さもだんだん落ち着き、念願叶って開発環境もWindows から MacOS & Linux になったので 「よし Mac に Qt の開発環境を準備しよう!」と思い立ってハマりまくったお話.

開発環境を作ると言っても、いつも通りインストーラー落としてきてインスコするだけだろうと思ったら QMake 時に「could not resolve SDK Path for ‘macosx’ 」と言って怒られてしまう…

最初にこんな物を見つけて、やってみるけどダメ…
https://stackoverflow.com/questions/26320677/error-could-not-resolve-sdk-path-for-macosx10-8/26321074

困り果てて、ダメ元でもう一度インストールしてみると Xcode 5.0.0 を入れろと警告してくる.  最初は「Xcode ? あぁー後で入れる入れる」と適当に流していたんですが、「Xcode 5.0.0 !? どんだけ古いねん! うぇーもしかして Qt の MacOS への対応ってすごい遅れてる???」とか思って、Apple の developper サイトから Xcode 5.0.0 を落として来てみるも、古すぎて Sierra にはそもそも入れられない…

転職でキャリアチェンジして仕事で Qt を弄らなくなってしまったというのもあり、その後も暫く空き時間やプライベートの時間に試してはダメを繰り返して、今日以下のポストを参考にやっと解決しました.

https://stackoverflow.com/questions/41513456/qt-5-7-xcode-8-1-os-x-el-captain-could-not-resolve-sdk-path-for-macosx

結果的に Xcode 上から一度どの Command line tool を使用するかの選択をしないと、Qt が参照している PATH 系の何かがセットされないような感じっぽい??

まさかこんなにどハマリするとは予想していませんでしたが、 とにかく情報が全然出てこないので Mac の Qt develper にはもしかして常識なのか、Xcode での開発をガン無視して初っ端から Qt で開発しようとしている自分がアレなのか…

ともかく毎度の事ながら stackoverflow さまさまでした… ありがたい