- 追加された行はこの色です。
- 削除された行はこの色です。
[[FrontPage]]
&size(30){&color(darkblue,#c0c0ff){''C++11がゲームプログラミングにもたらすもの''&br;};};
#counter
*はじめに [#uab6e7b1]
ゲームプログラミングの開発環境が変化してきました。~
かつてアセンブラやCで書かれていたゲームプログラムは、C++での開発が主流になったといえるでしょう。~
C#やJavaでのゲームプログラミングも現実的になってきましたが、本格的なゲーム開発はC++が多数派だと思います。~
なぜならば、ゲームは処理速度、メモリ効率、レスポンス、いずれも高い次元で動作することが要求されるからです。
C++11の登場で、C++でのゲームプログラミングも大幅に進化しました。~
それは、アセンブラからCへ、CからC++へと変遷していったときと同じぐらい、大きなインパクトがあります。
C++11により、C++のウイークポイントが解消され、実行速度、メモリ効率、開発効率、ともに大幅な改善がされたためです。
実際にゲーム開発でC++11を本格的に導入して2年ほど経過しましたので、
C++をゲームプログラミングで効率的に使う方法をまとめてみました。
** もくじ [#o69f245d]
- Chap.1 moveの活用
-- 1-1 右辺値参照のおさらい
- Chap.1 moveの活用
-- 1-1 右辺値参照のおさらい
-- 1-2 moveコンストラクタとmove operator = を実装
-- 1-3 std::swapの違い
-- 1-4 VisualStudioはmoveコンストラクタを自動生成してくれない
-- 1-5 落とし穴
--- 1-5-1 move後のクラスオブジェクトへのアクセス
--- 1-5-2 継承クラスのmoveコンストラクタ
- Chap.2 ラムダ式の活用
-- 2-1 コールバックの活用
-- 2-1 キャプチャの注意点
-- Chap.3 コピーコンストラクタの廃止
-- 3-1 コピーコンストラクタをmoveコンストラクタに置き換える
-- 3-2 shared_ptrをunique_ptrに置き換える
- Chap.4 応用編 描画ループの処理
-- 4-1 1/60秒の間隔で呼ばれるメインループ
-- 4-2 Threadもco-routineも使わないマルチタスク的処理
-- 4-3 タスクマネージャの紹介
-- 1-3 moveの落とし穴
--- 1-3-1 std::swapの違い
--- 1-3-2 move後のクラスオブジェクトへのアクセス
--- 1-3-3 継承クラスのmoveコンストラクタ
--- 1-3-4 VisualStudioはmoveコンストラクタを自動生成してくれない
- Chap.2 ラムダ式の活用
-- 2-1 コールバックの活用
-- 2-1 キャプチャの注意点
- Chap.3 コピーコンストラクタの廃止
-- 3-1 コピーコンストラクタをmoveコンストラクタに置き換える
-- 3-2 shared_ptrをunique_ptrに置き換える
- Chap.4 応用編 描画ループの処理
-- 4-1 1/60秒の間隔で呼ばれるメインループ
-- 4-2 Threadもco-routineも使わないマルチタスク的処理
-- 4-3 タスクマネージャの紹介
** CHAPTER-1 moveの活用 [#se0884ad]
moveの活用による恩恵は計り知れません。
とくに、速度とメモリ効率、そしてデバッグ効率を重視するゲームプログラミングにおいて、
moveセマンティクスの導入は大きな効果がありました。
なお、moveに関して十分な知識のある方ば、Chap.1は読み飛ばしてください。
*** 1-1 moveのおさらい [#va782358]
*** 1-1 moveのおさらい [#efa33848]
moveを使う前に、右辺値参照について知っておく必要があります。
このプログラムを見てください。
#sh(cpp){{
string a = "1";
string b = "2";
string c = a + b;
// ~~~~~
// 右辺値
}}
この"a + b"の部分が右辺値です。~
右辺値とは、名前のない一時的に生成されるオブジェクトのことです。~
この場合、string型で値が"12"の一時オブジェクトが生成されます。
わかりやすくC++03の書式で置き換えると、
#sh(cpp){{
string a = "1";
string b = "2";
string tmp(a + b);
string c = tmp;
}}
このような動作になります。~
tmpは式の外では不要になる、一時オブジェクトです。
さて、最後の"c = tmp"で行われるコピーが無駄な動作ということは明白ですね。~
C++03では、右辺値を変数に代入する時点で、コピーが発生してメモリと処理速度の無駄が発生していました。
コピーの無駄を省くにはtmpをcにエイリアスしてしまえはば解決しますが、
#sh(cpp){{
string& c = a + b;
}}
これはエラーになります。"a + b"は右辺値なので、参照型として使うことができません。
C++03では、メモリ上の何処かに生成された一時オブジェクトを、式の外へ持ち出す手段がありませんでした。
C++11では、右辺値参照という新しい機能が追加されました。
#sh(cpp){{
string&& c = a + b;
}}
これで、C++03で記述する以下の動作とほぼ等しくなります。
#sh(cpp){{
string tmp = a + b;
string& c = tmp;
}}
C++03では、右辺値として生成されたオブジェクトを使う場合、いったんコピーする必要がありました。
では、moveはどこでつかうかというと、
#sh(cpp){{
string&& c = a + b;
}}
とするかわりに、
#sh(cpp){{
string c = move(a+b);
}}
とすることで、"a + b"の一時オブジェクトをcに移動することが可能になります。
最初の例と大きな違いがないように見えますが、前者は(右辺値)参照、後者は移動(move)という違いがあります。
一般的には、moveのコストはcopyよりもずっと小さく、stringならばバッファのポインタとサイズをコピーするだけで終わります。
メモリ上にアロケートされた実体はコピーされずにそのまま使われます。
なお、上記の例はわかりやすくするために move(a+b)と書きましたが、a+bは明らかに右辺値なのでmoveは省略できます。
moveを明示的に使うのは、左辺値を右辺値に変換するときに使用します。
#sh(cpp){{
string a = "1";
string c = move(a); // cにaのインスタンスが移動する。
// これ以降はaにアクセスしてはならない。
// aは、ヌケガラ、デガラシ、捨てられたバナナの皮のようなもの。
// アクセスすると、未定義動作の洗礼を受けることになる。
}}
この例だと、aをcに移動させているだけで、なんのメリットもないコードです。
しかし、moveは後述するコンストラクタや代入演算子で必要になります。
*** 1-2 moveコンストラクタとmove operator = を実装 [#rb11e3d6]
*** 1-2 moveコンストラクタとmove operator = を実装 [#hf9f1781]
下記のプログラムは、C++03とC++11では動作が大きく異なります。
#sh(cpp){{
string c = a + b;
}}
先ほど解説したとおり、C++11では、"a + b"の一時オブジェクトはcにmoveされます。
なぜmoveされるのか? それは、stringにmove代入演算子とmoveコンストラクタがあるからです。
もし、自前のクラスで、moveコンストラクタやmove代入演算子が定義されていなかったら、moveされません。
#sh(cpp){{
Hoge a = 1;
Hoge b = 2;
Hoge c = a + b;
}}
この、3行目の"c = a + b"の動作は、Hogeクラスにmoveコンストラクタが実装されているか否かできまります。
http://melpon.org/wandbox/permlink/7v7e0TFsorASaOzQ
Hogeにmove代入演算子がなくても、ビルドはとおります。プログラムは何事もなかったかのように、COPYを行ってゆっくりと動作するでしょう。
目に見えにくいので注意が必要です。
moveを使うには、moveコンストラクタとmove代入演算子を定義する必要があります。
#sh(cpp){{
struct Hoge {
string str_;
Hoge(const Hoge& hoge) : str_(hoge) {} // コピーコンストラクタ
Hoge(Hoge&& hoge) : str_(move(hoge)) {} // moveコンストラクタ
Hoge& operator = (const Hoge& hoge) { str_ = hoge.str_; return *this; } // copy代入演算子
Hoge& operator = (Hoge&& hoge) { str_ = move(hoge.str_); return *this; } // move代入演算子
};
}}
これでOKです。~
インスタンスに対する操作が標準的な実装ならば、以下のように省略することができます。
#sh(cpp){{
struct Hoge {
string str_;
Hoge(const Hoge& hoge) = default;
Hoge(Hoge&& hoge) = default;
Hoge& operator = (const Hoge& hoge) = default;
Hoge& operator = (Hoge&& hoge) = default;
};
}}
http://melpon.org/wandbox/permlink/OHFJkkmV7XZllqL7
** 1-3 moveの落とし穴(地雷) [#j96c6229]
moveを活用することで速度とメモリ効率を同時に向上できますが、いくつか落とし穴があります。
実際に私が踏んでしまた地雷について解説します。
--- 1-3-1 地雷1 std::swapの違い
以下のコードをみてください
#sh(cpp){{
struct Hoge {
string str_;
...略
Hoge& operator = (Hoge hoge) {
swap(hoge);
}
void swap(Hoge& hoge) {
std::swap(*this, hoge);
}
};
}}
このコードは、C++03では問題なく動作しますが、C++11ではランタイムエラーになります。
その理由は、std::swapの内部実装にあります。C++03でのstd::swapは、
#sh(cpp){{
template<typename T>
void swap(T& a, T& b) {
T tmp = a;
a = b;
b = tmp;
}
}}
このように、代入を3回行うことでaとbを入れ替えています。~
これが、C++11になると以下のような実装に変わりました。
(cpp){{
template<typename T>
void swap(T& a, T& b) {
T tmp = move(a);
a = move(b);
b = move(tmp);
}
}}
コピー3回から、move3回に変わっています。~
先ほどのHogeクラスをよく見てください。operator = の中でswapを呼び出しています。
swapの中からは、operator =でインスタンスの移動を行っているので、operator = の動作はスタックを食いつぶすまでループしてしまいます。
いままで動いていたコードが、C++11にきり変えたことで動作しなくなる例です。
このような例はあまり多くありませんが、標準ライブラリはmoveへの対応で大幅に手が入れられていますから、注意が必要です。
c++11では、代入演算子でswapを呼び出すことは避けたほうが良いでしょう。
#sh(cpp){{
struct Hoge {
string str_;
...略
Hoge& operator = (const Hoge& hoge) { str_ = hoge.str_; }
Hoge& operator = (Hoge&& hoge) { str_ = move(hoge.str_); }
void swap(Hoge& hoge) { std::swap(*this, hoge); }
};
}}
これで正しく動作します。
--- 1-3-2 地雷2 move後のクラスオブジェクトへのアクセス
以下のプログラムは、何の問題もなく動くはずでした。すくなくともC++03までは。
#sh(cpp){{
void func() {
string a = hoge;
foo(a);
cout << a << endl;
}
}}
しかし、moveセマンティクスが導入されたC++11では、foo()の実装次第では安全でなくなります。
#sh(cpp) {{
void foo(string& a) {
string b = move(a);
//
}}
--- 1-3-3 地雷3 継承クラスのmoveコンストラクタ
--- 1-3-4 VisualStudioはmoveコンストラクタを自動生成してくれない
- Chap.2 ラムダ式の活用
-- 2-1 コールバックの活用
-- 2-1 キャプチャの注意点
- Chap.3 コピーコンストラクタの廃止
-- 3-1 コピーコンストラクタをmoveコンストラクタに置き換える
-- 3-2 shared_ptrをunique_ptrに置き換える
- Chap.4 応用編 描画ループの処理
-- 4-1 1/60秒の間隔で呼ばれるメインループ
-- 4-2 Threadもco-routineも使わないマルチタスク的処理
-- 4-3 タスクマネージャの紹介
}}