NRVO(RVO)とMove Semantics
ムーブセマンティクスについて調べたところで自分が気になったこと。
BigFatMovableObject GetBigFatMovableObject() { BigFatMovableObject mo; return mo; } int main() { BigFatMovableObject mo(GetBigFatMovableObject()); return 0; }
最初、自分はこれが適切なムーブコンストラクタを呼び出す、
と考えたんだけど、(どうみてもコピーよりムーブの方がコストが安い状況なので)
実際にはNRVOといった最適化によって、
「移動もコピーも発生しない状況」にされてしまった訳です。
これは駄目だ!!
と思ってvectorでやったわけですが、
意外とNRVO(RVO)って知られてないこともあるのかと思い書いてみる次第。
NRVOとは、
名前付き戻り値の最適化 (Named Return Value Optimization)
というやつです。
RVOは名前付きでないものです。
BigFatMovableObject GetBigFatMovableObject() { BigFatMovableObject mo; // 名前付き! return mo; }
BigFatMovableObject GetBigFatMovableObject() { return BigFatMovableObject(); // 名前なし! }
というような感じです。
こいつをどう最適化するかと言うと、
この最適化が働くと、「無用だと思われるコピーコンストラクタ」をコンパイラが削除します。
(削除しても「いいよ」ということになっています)
要するに「値返し」をするさいに本来であれば
1.関数内のオブジェクトの生成/破棄
2.戻り値のオブジェクトの生成(関数内のオブジェクトからのコピー)/破棄
3.関数を呼び出した側のオブジェクトの生成(戻り値のオブジェクトからのコピー)
といった無駄な手順を幾度も踏まなければなりませんが、
「このコピーって無駄だよね?」って感じでコンパイラが省いてくれるものです。
値返しのコピーコストが高いと思って使ってない人もコンパイラによってはちゃんとこれが働いてくれるので値返しでも構わないケースも多々あります。
(ただし規約で必ずそうしなければならない、というわけではなく、
「してもいいよ」なのでコンパイラ実装依存です)
NRVOが働かない状況だとこうなります。
C++ code
- 49 lines - codepad
----- GetBigFatMovableObject ----- BigFatMovableObject::BigFatMovableObject() Address:0x8051438
関数の中でオブジェクトが生成されました。
BigFatMovableObject(const BigFatMovableObject& rhs) Address:0x805b1a0 BigFatMovableObject::~BigFatMovableObject() Address:0x8051438
戻り値に対するコピー開始。
戻り値に関するコピーが終了したので関数内オブジェクト破棄。
BigFatMovableObject(const BigFatMovableObject& rhs) Address:0x8051438 BigFatMovableObject::~BigFatMovableObject() Address:0x805b1a0
main関数内のオブジェクトにコピー。
戻り値が破棄される。
----- main end. ----- BigFatMovableObject::~BigFatMovableObject() Address:0x8051438
main関数内オブジェクト破棄。
で、これはかなり悲惨な状況ですが、
gcc4.4だと
----- GetBigFatMovableObject ----- BigFatMovableObject::BigFatMovableObject() Address:0x100801000 ----- main end. ----- BigFatMovableObject::~BigFatMovableObject() Address:0x100801000
こうなっちゃうんですよね。
見事にコピーコンストラクタもムーブコンストラクタも省略されちゃいます。
なので、0xにちゃんと対応したようなコンパイラを使う場合、
こういうケースではムーブコンストラクタの出番はないかもしれません。
配列をポリモルフィックに扱ってはいけない
元々は
今日の仕組まれたバグ
こちらを見たときにずっと書こうと思っていたネタです。
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
とても邪悪な解き方
Cクイズ こっちわからん>< - So Many People, So Many Different Ideas
/* MyTypeの定義 */ void set(MyType m, int val); void inc(MyType m); void print(MyType m); int main(void) { MyType m; set(m, 0); /* mを0にセット */ inc(m); /* mをインクリメント */ print(m); /* 1と表示される */ return 0; } /* 各関数の実装 */ このmain関数が、コメントにある期待通り動くようにMyTypeを定義し、各関数を実装することができるか? もちろん、mainの中身は変えないこと。そして、使えるのは、C言語の言語要素のみ。
ぱっと思いついたもの。
/* MyTypeの定義 */ typedef int MyType; void set(MyType m, int val); void inc(MyType m); void print(MyType m); int main(void) { MyType m; set(m, 0); /* mを0にセット */ inc(m); /* mをインクリメント */ print(m); /* 1と表示される */ return 0; } void set(MyType m, int val) { *((int*)(&m + 0x08)) = val; // スタックよこんにちは(アドレスは環境依存 } void inc(MyType m) { (*((int*)(&m + 0x08)))++; // スタックよこんにちは(アドレスは環境依存 } void print(MyType m) { printf("%d\n",m); // えええええええ }
gcc 4.2.1で確認、とかそんなアホなことはない。
とっさにこういうのを思いつくのはイクナイ!!
* * * この答えはうそです + n ∧_∧ n + (ヨ(* ´∀`)E) Y Y *
マジレスするとtypedef int MyType[1];かな。