teppy のすべての投稿

privateメンバへ合法的にアクセス

プライベートは大切にしたいほうです。

このアーティクルを書いている今、個人的な話ですが仕事がデスマーチでプライベートなんてありません。

普段なら、この時期はプライベートジェットでサイパンに飛んで、プライベートビーチに寝転がってiPadでプライベートライアンを見ているはずです。

自分のプライベートが守られない今、他人のクラスのプライベートも守りません。積極的に攻めていくスタイルです。

雇われサラリーマンにも生活があります。

仕事の時間の他に自分の時間も必要なのです。そして、その部分というのは完全にプライベートなのです。

この従業員クラスをもうすこし価値のあるものにしてみましょう。

挨拶できるようになりました。

しかも挨拶する対象の職位に応じて挨拶を変えられます。

例えば

社長の時だけは休暇をせがむようテンプレートを特殊化しました。

 

 

では外部から、このEmployerのprivateメンバにアクセスしてゲームの時間をごっそり仕事の時間に移すことは可能でしょうか。

アクセス指定子の無効化というアーティクルの中で、privateメンバにもアクセスする術について記載しました。あれはC++の言語ルール的にはまったくの違法行為です。

privateというc++のキーワードを再定義してはいけません。

では合法的にやってみましょう。

メンバにテンプレート関数があると、外部でその関数の特殊化を行うことができます。これを利用して本来不可侵だったはずのprivateメンバの内容を意のままに書き換えました。

しかも、アクセス指定子の無効化と違い、今回のアプローチは合法です。特殊化したメンバ関数は、まったく議論の余地がない正式なクラスのメンバ関数ですのでc++の言語ルールに一切抵触していません。

この男は言語ルールの抜け道を知っている法律家だ。彼が捕まることは絶対にない。というのも、彼は法の精神を侵蝕する間も、注意深く条文に従っているからだ

__Exceptional C++ Style アクセス制御の使用と誤用

このアプローチが法に触れそうになる瞬間、それは特殊化に使用した型が既に別の箇所で同様の特殊化に使われており、結果この関数定義がODRに違反する場合です。しかしそれも無名ネームスペースに定義したオリジナルの型を用いることにより簡単に回避することができます。

まとめ

  • あるクラスがテンプレートメンバ関数を持っている場合、内部情報に合法的にアクセスできる
  • ODR違反を回避する為に特殊化に使用する型は無名ネームスペース内で定義する
  • プライベートは大事

テンプレートの特殊化

C++が他の数多ある言語と一線を画している特徴的な機能の一つにテンプレートという機構があります。

……と、書くと一部のクラスタから猛烈な抗議のメールを受け取ることになるので気をつけましょう。彼らはHaskellという一般の人には理解できない言語を日常的に使い、その特性にあるように完全に純粋で、自分たちを第一級オブジェクトとして扱う為、建設的な議論が困難です。

テンプレートの話に戻りましょう。

受け取った値を倍にして返すだけの関数ですが、組み込み型のint, floatはもちろん、ユーザー定義の型であっても、適切に*(int)演算子が定義されていれば倍にして返してくれます。便利ですね。

一度コードを書くだけで、型毎に自動的に関数を生成してくれる為、非常に柔軟で楽チンですが、それでも万能ではありません。

特定の型の時だけ振る舞いを変えたい場合も出てきます。

例えば文字列に対して倍にするって何だ?みたいな話があるので、文字列型に対してx2が呼ばれた場合は、受け取った文字列を2回繰り返した文字列にして返すようにしてみましょう。

std::stringを受け取った場合はこちらのx2が呼ばれます。

これをテンプレートの特殊化といいます。

この場合はx2()をstd::stringに対して特殊化しました。

 

まとめ

  1. 一部の型向けにテンプレート関数(or クラス)の実装を変えたい時はテンプレートの特殊化を行えばいい
  2. Haskellerには注意
  3. D言語界隈にも注意

ブレークポイントコード

これを見てピンと来た人は何人いるだろうか。

 

ああ、x86系CPU環境でブレークポイントと同じ効果をもたらすアセンブラコードだなーと思った人。正解ですが本質的ではありません。

ああ、闇夜のc++とか言いながらついにネタ切れでc++の話以外を書き始めたなーと思った人。正解だし本質的です。

xcodeやVisual Studioで以下のコードを実行してみましょう。

int 3というアセンブラ命令はデバッガへのトラップ用の割り込みを発生させるので、5行目で勝手にデバッガがブレークします。

そこからステップトレースも、再実行も可能です。

マクロを作っておくと何かと便利です。

より汎用的に使うためにglibでは環境毎にマクロ定義を切り替えてます。

glibのgbacktrace.hを覗いてみましょう。

環境依存ついでに、より環境依存な話をしておくとmsvc向けに__debugbreak();という命令もあります。やってることはint 3と一緒なので覚える必要はないでしょう。

環境依存なので3DS向けゲームの開発しかしてない人には無用の知識です。

まとめ

  1. c++について書くことが思いつかなくなった
  2. __asm int 3;はブレークポイント発生コード
  3. マクロ化しておくと使い勝手がいい
  4. 汎用的な実装がglibで提供されている

 

共変型の戻り値とオーバーライドルールの緩和

C++では、一般的に戻り値だけが異なるメソッドをオーバーライドすることはできません。

引数が違えば別関数と扱われ、単にメソッドが増えるだけなのですが、引数が同じ場合はオーバーライド対象なので、このコードはコンパイルエラーとなります。

ただし、戻り値の型が異なっていても、その型が共変型であるならばコンパイルエラーにはなりません。

オーバーライドする仮想関数の戻り値の型は、オーバーライド元の関数のものと同じか、または「共変(covariant)」でなければならない。

__C++ランゲージクイックリファレンス

……聞き慣れないですね。

要するに「戻り値の型がクラス型の参照かポインタで、かつis-a関係にあるもの」に関しては親クラスと異なる型の戻り値であってもオーバーライドできますよっていう話です。

例を見てみましょう。

何はともあれ、まずはis-a関係を持つ型を用意します。

先ほどのエラーになったオーバーライドの例の戻り値に今定義した型ShapeとBoxを指定します。

コンパイルは通りますが、共変型を持つメソッドはオーバーライドの挙動が通常のオーバーライドと異なるので注意が必要です。

以下のプログラムでは結果として”shape”と表示されます。

Wandboxで確認

Boxと表示されそうな気がしますが、実際にはshapeと表示されます。

ただし、ここが超重要なのですが、Shape*を返してもb->get()は決してBase::get()の呼び出しでは無いということです。

ポリモーフィズムの挙動に準じて、あくまでDerived::get()が呼び出されますが、操作しているポインタbの型次第で、Derived::get()で指定した型Box*のまま戻り値が返されるか、Base::get()で指定した型に暗黙に変換された後、返されるかという挙動の違いが発生します。

慣れないとちょっと分かりづらいですね。

むしろ結構分かりづらいですね。

ひょっとしたらC++の挙動の中でトップレベルの分かりづらさかもしれないですね。

 

一応、共変型に対してオーバーライドのルールが緩和される為、純粋仮想関数による子クラスへの関数定義の強制のメリットと実際に取得する型が親クラスに引っ張られないメリットを享受できる……ということになっています。

自分自身の型を返す関数を定義する時など、親クラス固定だと、受け取った側でのキャストが面倒ですしね。

共変型の戻り値には、常に適切な抽象レベルで作業できるという利点があります。(中略)共変型の戻り値を使用すれば、ミスを誘いがちなキャストを使用したために、そもそも失うはずの無かった型情報を再度提供するはめになることもありません。

__C++標準的コーディング技法#31 戻り値の共変型

 

まとめ

  1. 戻り値の型が違うメソッドはオーバーライドできません
  2. ただし共変型の場合のみこのルールが緩和される
  3. 操作している型のタイプによって戻り値の型は暗黙に変換される
  4. 型が暗黙に変換されるだけで関数の呼び出し自体はポリモーフィック

Pimplイディオム

オブジェクト指向の重要な概念のひとつに「カプセル化」というものがあります。

隠蔽化とも呼ばれ、ある機能を使いたい時に、ユーザーが実装詳細を意識しなくても動作し、ある日、実装の内容がドラスティックに変更されても、ユーザーは特に何もしなくても昨日までと同じようにその機能を使い続けられるということを期待するものです。

適切なカプセル化の思想の元に設計されたコードは変更に対して柔軟です。

あなたが学生時代に書いた、擬似乱数を生成するオブジェクトMyRandは昨日まで線形合同法で実装されていました。しかし、今日、急に思い立って擬似乱数の生成方法をxorshiftに変更したとしても、MyRandオブジェクトを使う側はその変更によって呼び出しコードを修正する必要はありません。

※この話はカルドセプトサーガおよび株式会社ロケットスタジオとは一切関係ありません。(http://www26.atwiki.jp/gcmatome/pages/2574.html

 

クラスの実装を呼び出し側から完全に分離し、隠蔽する手法にPimplイディオムというものがあります。

通常、クラス設計では公開するヘッダーファイルの定義の中にpublicからprivateまでの定義をひととおり書きます。

このクラスにメンバを足したり減らしたり、またはコメントを追加したり、何かしら変更を加えると、このクラスを使う(=ヘッダーファイルをインクルードしている)箇所全てが再コンパイルの対象になります。ライブラリの深いところのクラスをちょっと書き換えただけなのにフルビルドコースという話は決して珍しくありません。

また、ユーザーはクラスの公開されている部分にしか基本的にはアクセスできませんが、非公開関数のインターフェースとメンバ変数の詳細は見えています。

ああ、パスワードは最大16文字なんだな、とかCRC使ってるんだなとかの情報が読み取れてしまいますね。本例は多少作為的ですが、それでも外部に提供するクラスライブラリ等、なるべく情報を出したくないケースもあります。

データサイズとデータのレイアウトが想像できると、通信パケットやメモリダンプから情報を抜きやすくなってしまいますからね。これはもう圧倒的に抜かれます。

「公開されている部分にしか基本的にはアクセスできません」と書きましたが、基本的じゃない場合、その気になればメンバ変数pass_にも一瞬でアクセスできます。
アクセス指定子の無効化

クラス編集による再ビルドの影響範囲をなるべく抑えたい時やプライベートメンバをユーザーが見れる状況が好ましくない場合、Pimplイディオムを使ってクラスの実装を呼び出し側から完全に分離しましょう。

Pimplイディオム実装方法

ヘッダーファイル側の定義ではprivateメンバを書くのではなく内部実装の為のクラスのポインタのみ持ちます。

そして.cpp側でImplの詳細を定義することで実装を完全にヘッダーファイルから分離できます。

impl_経由で処理を呼び出します。

ちなみにPimplはピーインプルと読まれることが多く、Pointer To ImplementationやPrivate Implementationの意味で使われます。

Pimplイディオムは実装の詳細が分離できてコンパイル時間の減少にも貢献できる代償として、処理を呼び出す際にipml_を経由する過程で間接参照が必ず一回入るので、その分のオーバーヘッドのコストが発生する点だけ認識しておいてください。あと実際問題として、クラスの実装が委譲だらけになるので書くのが手間です。

まとめ

  1. Pimplイディオムを使うとクラスの定義と実装を完全に分離できる
  2. Pimplイディオムはコンパイル時間の抑制になる
  3. Pimplイディオムには間接参照のオーバーヘッドが発生する

アクセス指定子の無効化

世の中には知っていても「ほとんど悪にしかならない」知識があります。

例えば、簡易爆弾の作り方や、内鍵を糸だけで開ける方法などです。

さて、ここに誰かが定義したクラスがあります。

このクラスを使う側はpublic_func()しか呼べません。当たり前です。

private_func()にアクセスするにはどうすればいいでしょうか。

色々考えられます。

このクラスの設計者の家に押しかけてアクセスレベルの変更を迫るのも有力な手段です。また、我々はプログラマなので、お気に入りのエディタ(例えばEmacs)でおもむろにsomeone_defined.hppを開き、内容を勝手に書き換えてしまうのも悪くありません。

もっと(C++使い的に)スマートな方法もあります。

someone_defined.hppの内容を一切書き換えることなく、SomeoneDefinedクラスの内容に好きなだけアクセスする方法。

こちらを御覧ください。

はい、一丁上がりです。

someone_defined.hppは自分が何をされたのか気づいてすらいません。

この手法を悪ではなく正義の為に活かしましょう。(そんなものはない)

この悪魔のような手法からアナタの大切なクラスを守る方法はPimplイディオムを使って、完全に内部実装を隠蔽することです。

あと、悪魔には悪魔払いを!というわけで、こういう魔のモノを拒絶する方法もあります。

怪しいマクロを定義しているヤツにはインクルードさせてやらねーよ!ということですね。

ただし開発業務は進まなくなります。

 

まとめ

  1. アクセスレベルは簡単に書き換えられる
  2. この知識との上手な付き合い方は単に使わないこと
  3. 防衛策はPimplイディオム

おまけ

このprivateを再定義するアプローチは厳密にはc++のルールに違反している(c++のキーワードを再定義している)ので、本来何がおこっても不思議ではありません。たまたま無効化できているといったところです。

ちなみにある条件の下では、c++のルールに違反せずprivateメンバにアクセスする方法もあります。

参照:privateメンバへ合法的にアクセス

ローカルな文字列テーブル

関数内で使うローカルな文字列テーブル(配列)の定義には注意が必要です。

1月~12月までの月を数値で渡すと、英語の文字列を取得できる関数monthToEnglishを作ってみましょう。

例.第一引数に5を渡すとMayという文字列が第二引数のバッファに格納される

無事、完成しました。

有効範囲のエラー処理など細かい部分は端折ってますが適切な値を渡す限りは要件に見合った挙動をします。

さてこの関数ですが、大いなる無駄処理が走る可能性があります。

最適化を有効にした上で、アセンブラを見てみましょう。

41行目~に注目です。

MONTH_ENGLISH_TABLEはこの関数が呼び出される度にスタック上にコピーされます。ちなみにconst を constexprに変えても出力結果は同じです。

スタック上に値を生成したいという目的が無いのならば、このコードはこの関数が呼ばれる度にただただ不要な処理を走らせることになるのであまりよろしくありません。

例えばテクスチャ名のリスト、モーション名のリスト、気軽にローカルで定義することは良くあるかと思います。

そういう時に、gcc 4.9.2 で -O3ですら、前述のような(プログラマの意図に対して)無駄な処理が走ることは覚えておく価値があると思います。

この問題は定義にstaticを付けることで簡単に回避できます。

この場合のアセンブラコードを見てみましょう。

見違えるほどスッキリしました。

このように、関数ローカルな場所に文字列テーブルを作る時は呼吸をするようにstaticを付けるというプラクティスは悪くありません。

まとめ

  1. ローカルな文字列テーブルの定義は無駄なコストが生じる可能性がある
  2. ローカルな文字列テーブルにはstaticをつけよう

do-while(0)によるマクロラッピング

inline関数やconst変数を使ってもC言語時代に#defineマクロによって実現していたことの全てをカバーできるわけではありません。

今でもCPP(C Pre Processor)の力は偉大です。

特に、ビルドレベルに応じて違う挙動になるような処理を実現するには#defineによるマクロは有効な選択です。

以下のような要件を満たすマクロを考えてみましょう

  • 開発中のみ有効
  • ある条件を与えて結果が偽ならログ出力

以下のように使います。

DEBUG_LOGの定義を見て下さい。

マクロで展開したい実際の処理(std::printf)をdo{…}while(0)で囲んでます。

これはどういう意図でしょうか。

do{…}while(0)としたところで、内部のステートメントは1度しか実行されません。

do-while(0)の部分を省いて

と書くのと処理的には何も変わらないように見えます。

しかし実際には後者の定義は

  1. 行末の;(セミコロン)を強制できない
  2. 記述する場所によっては意図しない挙動になる
  3. ビルドレベルによって記述できる箇所が変わる

という3点の理由により全く不十分なのです。

順番に見てみましょう。

1.行末の;(セミコロン)を強制できない

do-while(0)じゃないバージョンでは行末の;(セミコロン)を強制できません。

このように展開されるのでセミコロンが無くてもc++のシンタックス上は問題の無いコードになります。(3-5行目)

しかしエディタ上での見た目は

となります。これはc++のシンタックスに準じていないし、コード全体の一貫性も欠くことになります。しかし、そう書けてしまうというのは使う側ではなく、定義した側の責任です。

2.記述する場所によっては意図しない挙動になる

このマクロがブロック無しif文の直後で呼ばれた時に意図しない挙動になります。

ブロック無しif文なんて書くな、カッコ悪いぞ!っていう話ではあるのですが、こう書いた時に以下のように展開されます。

if (p)と対だったはずのelse節がマクロから展開された方のif文との対になってしまいました。

これも使う側ではなく、定義した側の責任です。

do-whileで囲んだ最初の定義だと以下のように展開されます。

 

3.ビルドレベルによって記述できる箇所が変わる

マクロが展開を展開した時にシンタックスエラーになるケースが変わります。

if文だけのマクロの場合はこのような書式がc++的に許されてしまいます。

しかもデバッグビルド時は問題無いのに、いざリリースしようとするとコンパイルエラーになってしまいます。

同じことが非デバッグ時のDEBUG_LOGの定義にも言え、例えば((void)0)を書かずに、展開結果をただ空白にしてしまうと、非デバッグビルド時には、どこにでも記述できるというマクロになってしまいます。

このように書いてもエラーになりません。

マクロはc++のシンタックスを無視して、どこにでも記述できてしまいます。どこにでも記述させたい明確な理由が無い場合、このような記述はできるべきではありませんし、そもそもデバッグビルド時と記述可能箇所のルールが変わってしまいます。

ビルドレベルによって記述可能箇所のルールが変わるのは、単に分かりづらい上に、ビルドレベルを変えたタイミングで、初めてコーディングのミスに気づくことになるので効率も悪いです。

どういうスキルレベルのプログラマに使われても不具合が起こりにくくなるように、可能な限り、紛れの無いコーディングを促すような設計を心がけましょう。

まとめ

  1. 展開された時に意図しない挙動になるのを防ぐ為、マクロをdo-while(0)でくくるのは良いプラクティス
  2. ビルドレベルが変わってもマクロを展開できる箇所には一貫性をもたせるべき

非public継承の使いどころ

継承には3つのレベルがあります。

  • public
  • protected
  • private

オブジェクト指向的な考え方ではpublic継承はよくis-a関係を表すなどと言われたりしますね。

どら焼き is a 和菓子。

ベジータ is a サイヤ人。

B is a A(びーいずあえー)という関係なのでis-a関係です。

一方、private継承、protected継承はis-a関係ではありません。

is-a関係との対比でhas-a関係、またはis-implemented-in-terms-of関係なんて呼ばれたりします。

public継承以外はB is a Aの関係では無いため、AのポインタにBを代入することはできません。

基底クラスのポインタへの代入を許すのはpublic継承だけです。

public継承以外の継承では基底クラスのメンバへは内部からしかアクセスできません。ですので、あるクラスをprivate継承していようが、メンバとしてもっていようが、Bというクラスの実装の詳細の為に内部でAの機能を使うという状態は実際のところ変わりません。

つまり、あるクラスの持つ機能を使いたい場合には

  1. メンバとして持つ
  2. 継承する

という2つの選択肢があります。

基本的に継承はコードの複雑性を増すので避けられるなら避けた方が望ましいとされています。できるだけコンポジション(内包)を使って実装する方が良いでしょう。

それでも継承を使って実装するシチュエーションはなんでしょうか?

1つには、EBO(Empty Base Optimization)が効くケースがあるという点が挙げられます。これはメモリ効率の観点からすると大きなアドバンテージとなります。

2つ目は、あなたがライブラリの設計者で、自作するクラスBは、Aというクラスの仮想関数をオーバーライドして何かしたいけど、ユーザーには自分の作ったクラスをAのポインタとして管理させたくない。という場合です。何を言っているのでしょうか。

これで画面には”B-do”と3回表示されます。

あまり実用性を感じない例ですが、あるクラスの一部の挙動をオーバーライドでカスタマイズしたい場合は継承するしかありません。このようなケースは本来ならpublic継承でいいのですが、なんらかの理由(基底クラスのデストラクタがvirtualになっていない等)で基底クラスのポインタによる管理を許さない設計にしたい場合は前述のとおり、public継承しないことで実現できます。

STLのコンテナは全てデストラクタがvirtualではありません。ですのでこれらのクラスを拡張した便利コンテナクラスを作成したい場合も絶対にpublic継承してはいけません。とはいえ、例えばstd::vectorの拡張クラスを作りたい時に、std::vectorをメンバとして持つと、std::vectorの持つメソッドを全て再実装しなければいけないという手間が発生します。

上記のようなケースでは継承とusingを組み合わせることで実装がシンプルになります。

boost::noncopyableはデータを持たないクラスなのでEBOが効き、さらにデストラクタがvirtualではないので、private継承にうってつけの例と言えます。

まとめ

  1. public継承以外は基底クラスのポインタへの代入はできない
  2. デストラクタがvirtualではないクラスは絶対public継承してはいけない
  3. 継承ではなく内包で実装できる時はなるべくそうしたほうが良い
  4. 非public継承すべき時は特殊だが存在するので、c++の挙動を正しく理解して判断する

virtualの伝播

このBoxクラスをpublic継承しても安全でしょうか?

正解は……この情報だけでは何とも言えない。でした。

一見するとダメっぽいと感じる人も多いかと思います。特にc++のことをちょっと詳しくなった頃の人はまずデストラクタに目が行くかと思います。

デストラクタの前にvirtualが付いていませんね。virtualで無いデストラクタは継承してはいけないということはc++で最初に学ぶことの一つです。

もう少し情報を足して、Objectクラスの定義も見てみましょう。

シンプルです。

virtualが定義されたデストラクタが定義されている模様です。

これではっきりしました。Boxはpublic継承しても安全です。

「Boxをpublic継承しても安全」という表現をもっと詳しく書くと「Boxを継承したクラスをnewで生成して、そのポインタをBox,もしくはObjectのポインタに入れて、deleteを呼んでも安全」ということです。

 

Objectクラスのデストラクタにvirtualが付いているので、Objectをpublic継承している全てのクラスのデストラクタは暗黙にvirtual指定が付きます。

C++では基底クラスでvirtualで定義されたメソッドは、派生先でも 、もれなくvirtualになります。

 

一昔前は派生先のメソッドにvirtualをつけることで、継承元のメソッドをオーバーライドしてますよ的なことを匂わせるアプローチもありましたが今はc++11から追加されたoverrideキーワードがあるのでそちらを使いましょう。

オーバーライドしている意思が明確になり、より可読性の高いコードとなります。また、メソッド名を間違えたりしても、今までは間違った名前で新しいメソッドが作られるだけで不具合の温床になっていましたが、overrideキーワードの恩恵で、基底クラスに無いメソッド名を指定した場合はエラーにしてくれます。素敵ですね!

まとめ

  1. 基底クラスでvirtualだったものは派生先では勝手にvirtualになる
  2. オーバーライドを明示するためにc++11から追加されたoverrideキーワードを使おう