読者です 読者をやめる 読者になる 読者になる

配列をポリモルフィックに扱ってはいけない

元々は
今日の仕組まれたバグ
こちらを見たときにずっと書こうと思っていたネタです。

num が 1 の状態でしか確認されてなくて、いざ使うことになって2以上にした途端に Segmentation fault するとか嫌がらせだろw ただの凡ミスなんだろうけど、なんかテストに出そうな問題だなーとか思ったのでネタにしてみた。

これ、ほぼバグになるんだから警告ぐらい出てくれてもいいのに、と思わなくもない。配列 new とアップキャスト。こんなにシンプルなアップキャストでメモリを壊そうとできる言語ってそうは無いよなーw これだから C++ 大好きだwww

C++はどうしてもメモリに依存するところが大きいのと、
クラス情報が最小なので致し方がないところではありますが、
こういうのは警告でても良いよね、と自分も思います。(PODでないクラスを見極めて!


で、このバグの面白いところ(嫌なところ)は「うまく動いてしまうケースもある」ということです。
うまく動くケースは1つ。
「継承元と継承先のsizeofが一致する場合はうまく動く(可能性が高い)」
ということです。それは、たまたま触るべきポインタに矛盾が生じないからです。

なぜ、というとこのバグが起こる理由がC++ではクラスのポインタからそのクラスの実体のsizeofは解らない、ということにあります。
(Base*が実際にDerivation*を指していたとしてもコンパイラに解るのはsizeof(Base)だけだということです)

では、実験実験。
http://codepad.org/AM0EpDuw

これはあえてsizeof(Base) == sizeof(Derivation)
にしているケースです。
コンストラクタもメンバ関数もデストラクタもちゃんと呼び出されています。

ですが、これが
http://codepad.org/fJSS7dUv
sizeof(Base) != sizeof(Derivation)になるとあっという間に
Segmentation fault
です。
(この例ではgccのようにメンバを増やしてもクラスの最低alignmentを守って同一サイズになるのを防ぐためにint hoge_を付加しています)

これはどうして起きるのでしょう?
先ほども書きましたが、
コンパイラ
Base*から得られる情報がsizeof(Base)であるということに起因します。
コンパイラ
Base*からsizeof(Base)のサイズでクラスが並んでいると仮定します。
なので、
d[i]でアクセスされる領域はsizeof(Base)単位です。
故に、
dが実際にはDerivationを指していてもsizeof(Derivation)単位でアクセスしなければならないことが解りません。


上記のケースでは、Derivation:20byteなので、
Base*から20byte単位でアクセスしなければDerivationに正しくアクセスできません。
が、コンパイラはBase*なのでBase:12byte単位でアクセスしてしまう訳です。


ここで起きること
・仮想関数でない関数を呼び出した場合thisはこのsizeof(Base)単位で呼び出されたアドレスになる。故にメンバ変数は全て不正になる
・仮想関数を呼び出した場合、仮想関数を呼び出す際に利用するvptrの位置が不正になるのでほぼかっとぶ。
・仮想デストラクタを呼び出した場合、上記理由によりほぼかっとぶ。
です。


対策。
・クラスを配列でnewしてはいけません(いけないことはないのだが
・どうしてもクラスを配列でnewしなければならないときは基底のポインタに入れてはいけません
・なので、クラスを配列でnewした場合、基底のポインタでdeleteしてはいけません


配列はポリモルフィックに扱えないものなのです。
そして、それはC++がポインタに対して「実際にポインタが指しているクラスのsizeofを明らかにする方法がない」ということに起因しているのです。


これだから C++ 大好きだwww