C++11 とオブジェクト指向
これは C++11 Advent Calendar 2011 の 3 日目の記事です。*1
C++03 から C++11 になったことで大小さまざまな言語仕様拡張・変更がありましたが、それらが C++ におけるオブジェクト指向プログラミングをどう変えてゆくのか、現段階で思うところを書こうと思います。
・プロローグ 「C++11 と魔法少女まどか☆マギカ」
C++11 の 11 は 2011年 のことです。そして魔法少女まどか☆マギカは 2011年 の日本を舞台にした大ヒットアニメです。偶然でしょうか?いいえ、これらが無関係なわけがありません。
さて、C++ に追加された機能で最も強力なのが右辺値参照だと思います。これについては gintenlabo(@SubaruG) さんや, alwei(@aizen76) さんが後日のアドベで詳細を書いてくれると思います。しかしそれら機能の多くはライブラリアンのために提供されており、末端のプログラマにはあまり関係がありません。僕のような下っ端のプログラマに最も強く影響するのは move() ではないでしょうか。
move() というと何やら難しそうですが、
move() は、簡単に言うと引数をマミる関数です。
そう、実は C++11 の言語仕様にはマミさんのソウルジェムが輝いているのです。(嘘です)
move のサンプルはもう散々目にしたかもしれませんが、コードにするとこうなります。
struct A { int data_ }; int main() { A a1 { 42 }; A a2 = move( a1 ); // ここでマミる。a1 はもう使えない。 }
では本題に入ります。
・第一話 「派生」
派生は、多くのオブジェクト指向プログラミング言語で取り入れられている概念です。
class Base {}; class Derived: public Base {};
Derived は Base を完全に含んでおり、コンストラクタが呼ばれると内部に Base オブジェクトを生成します。しかし思うに、Base 部分は外部から調達したって良いような気がします。コードを↓このように修正してみます。
class Base {}; class Derived: public Base { public: Derived( Base const & b ) : Base( b ) {} // コピー }; int main() { Base b; Derived d( b ); }
しかし、この方法を採りたく無い場合があります。
もし Base が外部リソースのハンドルを(例えばファイルを開いたまま)握っていたりしたなら話がややこしくなってしまいます。なぜなら↓図のように Base のコピーが生成されているからです。*2
そこで C++11 の move() を使います。↓図のように Base をまるごと Derived の Base 部分に差し込んでやることができます。
コードにすると↓こうですね。
class Base {}; class Derived : public Base { public: Derived( Base && b ) : Base( move( b ) ) {} // 差し込む。 }; int main() { Base b; Derived d( move( b ) ); // ここでマミる。b はもう使わない。 }
素晴らしい。Java でこういうクラスは定義できないですよね。
カプセル化を維持したまま、生成タイミングをユーザーの都合に合わせて段階的にずらせるようなクラスを表現できました。
キモいですか? いえ、大丈夫です。「基本クラス部分を右辺値参照で受け取って差し込む」という手続きが Derived の責任のもとで執り行われます。この考え方はオブジェクト指向の基本的な考え方に合致します。
アホみたいな話で恐縮なのですが、どうも僕には
ように思うのです。ちょっと Java のコードを書いてみますね。
「豆腐を切ったら豆腐 2 つになる」ということを、Java は表現できない
// 豆腐。 class Tofu { private int weight; public Tofu( int weight ) { setWeight( weight ); } public int getWeight() { return this.weight; } public void setWeight( int weight ) { if( weight <= 0 ) { throw new IllegalArgumentException( "重さの無い豆腐は豆腐では無いぃぃぃぃッ!" ); } this.weight = weight; } } class Main { public static void main( String[] args ) { Tofu t = new Tofu( 100 ); // 100g の豆腐がある。 Tofu[] sliced = Slicer.slice( t, 2 ); // 2つに切る。 System.out.println( sliced[ 0 ].getWeight() ); // 50g System.out.println( sliced[ 1 ].getWeight() ); // 50g System.out.println( t.getWeight() ); // いくつになるべき? } }
最後の文で、もし t が 100g なら「豆腐を切ったら豆腐の総量が増えた」ことになってしまいますし、t が 0g ならそれは既に豆腐ではありません。こんなの絶対おかしいよ!
オブジェクト指向プログラミングを紹介する文脈では、よく「現実のモノに名前をつけてモデル化するのがクラスで...」といいますが*4、その典型である Java のモデル表現能力は、この例が示すようにさほど高くないように思うのです。
しかし C++11 には move() があります。
int main() { Tofu t { 100 }; // 100g の豆腐がある。 vector< Tofu > sliced = Slicer::slice( move( t ), 2 ); // ここでマミる。 cout << sliced[ 0 ].getWeight() << endl; // 50g cout << sliced[ 1 ].getWeight() << endl; // 50g cout << t.getWeight() << endl; // そもそも t にはアクセスしてはならない。 }
素晴らしい。main 関数を書いたプログラマが「豆腐 t は、Slicer::slice に渡したら消えて無くなる」という旨をコード上で正しく表現できるわけです。
(当初、さやかの手足が切られる例を用意していたのですが、グロかったので豆腐にしました)
■第三話 「定数オブジェクト」
例えば double const や enum など、定数を定義することはよくありますが、組み込み型だけが定数ではありません。ユーザー定義型のオブジェクトを定数とみなすこともあるかと思います。
例えば↓こんなやつ。
// 色。 class color { public: int r_value, g_value, b_value; }; int main() { color const yellow { 255, 200, 30 }; // yellow.r_value は 255 である。 assert( yellow.r_value == 255 ); }
さて、本当に定数なら↓これが動いてナンボでしょう。
int main() { color const yellow { 255, 200, 30 }; int n = input(); switch( n ) { case yellow.r_value: cout << "r と同じ"; break; case yellow.g_value: cout << "g と同じ"; break; case yellow.b_value: cout << "b と同じ"; break; } }
残念ながらコンパイルエラーです。いくら定数といえど、構文上は const 修飾された変数と全く同じなので case 値にすることはできないんですね。C++03 まではこうした「定数のようなオブジェクト」をどう頑張っても本当の定数にすることはできませんでした。
そこで C++11 からは、新しく constexpr というキーワードが利用できるようになりました。constexpr の詳細は boleros(@bolero_MURAKAMI)さんや iorate(@iorate)さんが関連記事を書いてくれる予定ですが、
簡単に言うと constexpr は「これは定数だ」とコンパイラとの間で契約を結ぶものです。
そう、契約....
「僕と契約して、
そう、C++11 プログラマは皆、魔法少女なのです。(嘘です)
constexpr は、関数や変数に適用してコンパイル時に定数値を決定づけます。↓こんな感じ。
int main() { constexpr color const yellow { 255, 200, 30 }; int n = input(); switch( n ) { case yellow.r_value: cout << "r と同じ"; break; case yellow.g_value: cout << "g と同じ"; break; case yellow.b_value: cout << "b と同じ"; break; } }
素晴らしい。コンパイルが通りました。
また契約といえば assert ですが、C++11 では static_assert という静的な assert も使えるようになりました。
こうしてコンパイル時定数の表現能力が上がるということは、C++ の強力なテンプレートメタプログラミングの威力が増すということであり、最適化が増強されるということでもあります。これらを組み合わせることで定数オブジェクトの利用シーンは確実に増えるはずです。どんどん使っていきましょう。
■いじょ。
C++11 らしいところ、ということで 3 つほど出しましたけど、探せば何かもっと面白いネタは有りそうな気がします。(ところどころ Java を反面教師として出してますが、僕は Java は嫌いじゃないですよ。)
それでは、次のDigitalGhost(@decimalbloat)さんの記事を楽しみに待ちましょ。