カテゴリー別アーカイブ: Language

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言語界隈にも注意

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

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. 型が暗黙に変換されるだけで関数の呼び出し自体はポリモーフィック

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キーワードを使おう

 

newの実装

newは引数でサイズを受けますが、標準のnewはsizeが0でも正しく動作することを規格で定めています。

自分で実装する際には意識するとちょっとだけ規格に準じたコードになってよいです。

これ豆な!

まとめ

  1. 標準のnewはサイズ0にも対応している
  2. 完全に準じるにはそこそこの知識が必要なので、せめてこれだけでも覚えておくと良い
  3. そんなレアケースに対応することでnewにコンペア命令が増えるなんて勿体無い!という人も教養として覚えておくと良い

禁忌の識別子

名前を付けるという行為には責任が伴います。

子供にせよ、ペットにせよ、変数名にせよ。

ドラクエ5で子供の名前を付ける時に、かなり悩んだ経験を持つ人も多いのではないでしょうか。

そしてC++でコーディングしている時も名前を付ける機会は多いです。

私の人生で、何かに名前を付けるという機会が数多くありましたが、その99.999%はプログラミングしている時でした。

関数名、変数名、マクロ名にクラス名……他にもとにかく名前を付けなければいけないタイミングは多いです。その都度、ドラクエ5の時くらい悩んでいてはプロジェクトは全く進まなくなってしまうので、直感と経験でサクサク名前を付けましょう。

ちなみにここで言及している「名前」のことをC++的に気取っていうと「識別子(Identifiers)」といいます。

この識別子には色々ルールがあって、ルールに違反した識別子は使ってはいけません。

エラーになる識別子

識別子のルールに違反した場合、コンパイラがエラーにしてくれるものがあります。

  • 数字から始まる識別子は使えません
  • C++キーワードとして登録されている文字列は識別子には使えません
  • 一部演算子の代替表現も識別子には使えません

 

一部演算子の代替表現?なんだそれは!?

C++ではキーワードではないが、ある種の演算子と同意語の単語がいくつか用意されており、それらも識別子として使用できません。

例えば && の代替表現は and で、 | の代替表現はbitor です。

Furthermore, the alternative representations shown in Table 5 for certain operators and punctuators (2.6)
are reserved and shall not be used otherwise:

Table 5 – Alternative representaions

and and_eq bitand
bitor compl not not_eq
or or_eq xor xor_eq

__n3337 2.12 keywords.

 

 

これらのルールはコンパイルの段階でエラーになるので、詳細を覚えててなくても大丈夫です。

 エラーにならない識別子

以下のルールはエラーにはなりませんが、C++の予約語のため、使ってはいけません。

  • アンダースコアで始まり大文字が続く名前(コード中どこでも禁止)
  • アンダースコアが連続する名前(コード中どこでも禁止)
  • アンダースコアから始まる名前(グローバル変数のみ禁止)

これらのルールに違反した識別子を使った場合の動作保証は無いので、絶対に使わないようにしましょう。例え__Japan_987654321_wwww___ssl93jjasf__なんて識別子が被るなんてあるわけないから別に問題ないじゃん!と思ったとしても、避けられるリスクは避けるべきです。単に言語の規格で使うべからずとされているものは使わない方がいいのです。

特にインクルードガードを書く時にこのルールを犯しているコードをよく見かけます。

コンパイラがエラーにしてくれないので、書く側で気をつけましょう。

まとめ

  1.  キーワードと同じ名前は識別子にはつけられません
  2. 記号の代替表現と同じ名前は識別子にはつけられません
  3. 数字から始まる名前は識別子にはつけられません
  4. 予約語となっているルールを破る識別子は絶対につけてはいけません

インクルードガード

c++の#include ディレクティブは、プリプロセスで解釈され、書かれたファイル名の内容を単にその場所に展開したものとして処理されます。

 

一般的にファイルの先頭付近にinclude文はまとめて書く感じですが、コードの中に埋め込むことも可能です。

たとえば以下のような、動物の名前を列挙しただけのファイル(animals.csv)があるとします。

馬、狼、カンガルーと書いてあるだけですが

とすると、

と展開されて、普通にコンパイルが通ります。

このように#includeディレクティブはとにかく機械的に展開するものだ、という話です。

で、例えば普通に同じヘッダーファイルを複数includeしてしまうと、(機械的に展開するだけなので)includeの数だけその場所に展開されます。include元のファイルに何かシンボルの定義みたいなものが含まれている場合、多重定義エラーになります。

hoge.hpp

main.cpp

なので、一つのコンパイル単位中に同じファイルが複数回、展開されないようにする必要があります。

多重に展開されることを防止するには、展開元のファイルに多重展開防止用のマクロ(インクルードガード)を仕込むのが定石とされています。

c/c++が標準で提供する全てのヘッダーファイルにはインクルードガードが使われています。

インクルードガード

シンボルの定義名(例ではINCLUDE_GUARD_HOGE_HPP)は、一意に定義される文字列なら別に何でも良いです。

複数includeされても、最初の一回以外はINCLUDE_GUARD_HOGE_HPPが定義済みなので単にスキップされますので、include元のファイル内に書かれたクラス定義などが多重定義されてしまう心配はありません。

冗長インクルードガード

先ほどの例のようにinclude元のファイルの先頭と末尾を#ifndef -#endifで囲うインクルードガードを、厳密には「内部インクルードガード」といいます。

そして、この#ifndef -#endifの部分をinclude先にも書く手法を「冗長インクルードガード」といいます。

このように書くと#include文の評価をする前に、スキップするか決められる為、コンパイル時間の短縮につながる場合があります。

困ったことに、コンパイル時に複数のヘッダーファイルが何度も検出されるような状況下では、ヘッダーファイルを開き、#ifndefを評価し、終わりの#endifを走査するプロセスに時間がかかる可能性があります。場合によっては冗長インクルードガードを設定することにより、コンパイル時間を大幅に短縮できます。

__C++標準的コーディング技法 #62:インクルードガード

冗長インクルードガードについては毎回外部に書く手間の割に大したメリットも無いのであまり使われていません。むしろガードにつかうマクロ名を内部インクルードガードと冗長インクルードガードで合わせなければいけない為、別々のファイルなのに結合が生じてしまいます。内部インクルードガードのマクロ名を変えた場合、冗長インクルード側のマクロ名も忘れずに変えなければなりません。

外部の#includeガードは冗長で、現在のコンパイラ上では時代遅れである。また、インクルードする側とヘッダーファイルがガード名について合意していなければならない。これにより、密接な結合が生じ、脆弱となる。

__C++ Coding Standards

今の時代には不要となった冗長インクルードガードですが、教養として押さえておくと良いでしょう。コモンセンスってやつですね。

#pragma once

言語機能ではありませんが、多くのコンパイラではこの魔法の言葉をファイル中に書いておくだけで複数インクルードされない仕組みを提供しています。インクルードガードを書くより、遥かにタイプ数が少なく済むので単純に楽です。

しかし、#pragma onceによる複数include回避の方法はコンパイラに依存するため、コンパイラによっては使えないこともありますし、コンパイラの実装によっては予期せぬ不具合の原因になる場合があります。

例えば、あるファイルを複数includeしない為に、そのファイルが既にincludeされたことがあるかをコンパイラは覚えておく必要があります。include済みのファイルかどうかを判定するために、ファイル単位に一意のシンボル名を定義するとします。その一意の文字列生成の為にファイルパスを埋め込んでシンボル名を作るような実装になっていた場合、すごーーーく長いパス(c:/user/xxxxxxxxxxxx/development/my_project/tekitou_na_project_name/game/lib/include/group/category/tekitou_na_header_file_name.hpp)の場合、途中でバッサリとパスが切られて、まったく同じシンボル名になって、初めてincludeしたファイルなのにスキップされるという、なかなか原因解明が困難な不具合に襲われる……といったケースがあるかもしれません。

……はい、体験談です。

もともと、言語標準じゃない機能なので私は使わなかったのですが、あの一件以来、さらに避けるようになりました。

コンパイラによる拡張機能で、言語の機能ではないので、これをコード中に書いた瞬間に、そのコードはコンパイラ依存のコードになります。ポータビリティの高いコードを書きたい場合は、単に使わない方が得策です。

#pragma onceを使わなくても、マクロによるインクルードガードで、コンパイラ依存の無い形で実現出来ますからね。

#pragma onceも冗長インクルードガードと同様、includeするファイルの中を見に行く前に、以前にincludeしたことがあるかを判定出来るため、若干コンパイル時間が短くなることが期待できます。

実例

初学者はC++の勉強の一環として、CryENGINEのソースコードを読んだりすると思いますが、その中でもこのインクルードガードに触れることができます。

以下はCryENGINEのあるヘッダの冒頭部分です。

インクルードガードのマクロ名が__で始まっていますが、連続するアンダーバーを含む名前はC++の予約語なので真似するのは絶対に止めましょう。

参照:禁忌の識別子

スーパープログラマ集団でも平気でC++の規格に違反してしまうところを垣間見て勇気を得た所で先に進みます。その下でVCのバージョンを見た上で#pragma onceしているのが分かると思います。

#pragma onceが効く環境では#pragma onceによって、ちょっとでもコンパイル時間を短縮しようというという貪欲な姿勢が見てとれますね。このようにインクルードガードとの合わせ技で攻めるというアプローチもあるので覚えておくと良いかもしれません。

 

まとめ

  1. #include ディレクティブはプリプロセスでファイルを機械的に展開するだけ
  2. 多重includeを防止するためにインクルードガードを書こう
  3. インクルードガードには内部と冗長の2種類がある
  4. コンパイラ拡張の#pragma onceによっても多重includeは防止できる
  5. (おまけ)インクルードガードに使う定義名はc++のルールの中で決めよう

Plain Old Data

C++におけるPOD(Plain Old Data)とは、C言語のデータと互換を持つデータ構造のことです。

memcpyでデータをコピーできたりするものですが、それは本質的ではなく、とにかくPODの一番の意味はC言語のデータとbitレベルで完全に互換を持つということです。C++とC言語をまたぐプログラムがある場合、受け渡されるデータはPODでなければいけません。

会話中での出現例でいうと「あれ、ちゃんとPODで作っといてよ!」という具合に使います。

では、PODとは具体的にはどういうデータ構造でしょうか。

C互換データ=PODは、複雑なクラスレイアウトや、構築、コピー、ムーブなどのユーザ定義のセマンティクスを配慮することなく、”単なるデータ”として利用できるオブジェクトである。

__プログラミング言語C++第4版 8.2.6 POD

POD型の定義

  • 標準レイアウト型(standard layout type)である
  • トリビアル型(trivial type)である

この2つの定義を満たせば、それは紛れもないPOD型です。

簡単ですね!

それぞれの定義をさらに掘り下げます。

標準レイアウト型

  • virtual関数を持たない
  • virtual継承をしていない
  • 参照メンバを持たない
  • 非staticメンバに対して複数のアクセス指定子を持たない(重要!)
  • 標準レイアウトでない型を継承していない
  • 標準レイアウトでない非staticメンバを持たない
  • 継承していてもいいが非staticメンバを持つクラスは継承ツリーの中で1つだけしかない

4番目の項目に注目してください。クラスの不定なメモリレイアウトでも触れたとおり、複数のアクセス指定子でメンバを定義してある場合、標準レイアウトの保証はなくなります。

トリビアル型

  • トリビアルなデフォルトコンストラクタを持つ
  • トリビアルなコピー動作およびムーブ動作をする

散々登場しているこの「トリビアルな○○」ってなんやねん!と、あなたは言うでしょう。分かっていますよ。

トリビアルなデフォルトコンストラクタを持つ、トリビアルなコピー動作、ムーブ動作をするとは、大雑把に言うとコンパイラ定義のこれらの動作をするということです。

例えば、コピーコンストラクタをユーザーが定義したら、それはもうトリビアルな型ではありません。

引数無しコンストラクタをユーザーが定義したら、コンパイラ定義のデフォルトコンストラクタが無くなるのでこれもトリビアルな型ではありません。

 

さあ、これで自分の書いたクラスがPODかどうか判断できるようになりました。でもこんなことは忘れてもいいのです。

あるクラスがPODかどうか、本当に知りたいのならstd::is_podを使いましょう。

 

(コンパイラベンダーの実装にバグが無い限り)正確にPODの判定をしてくれます。

 

小難しいことぁいいんだよ!という人へ。

それほど間違っていないPOD型

  • 自分でコンストラクタとか定義してない
  • virtualという文字列を打ってない
  • 継承してない
  • メンバ変数はprivate(もしくはpublic)にしか定義してない
  • 組み込み型(int, char, float…etc)しかメンバに持ってない

とりあえず、これを抑えておけば大概はPODです。それでもPODじゃないデータ構造を定義してしまったら、逆にすごくね?と思うようにすると良いでしょう。

これはPOD。

これもPOD。

あと、おまけですがPODじゃない型のstaticメンバを持っていてもそのクラスのPOD性には影響ありません。

まとめ

  1. PODはC言語互換のデータ構造
  2. PODの定義は細かく定められている
  3. c++11ボーイエンドガールズはstd::is_pod<T>::valueで判定しよう