C++初心者向け講座その1 ~RAIIのお話~
概要
C++の初心者向けに、クラスを利用したリソース管理及び特殊なメンバ関数であるコンストラクタ、デストラクタ、コピーコンストラクタ、ムーブコンストラクタと、デフォルトで定義されうるコピー代入演算子、ムーブ代入演算子について簡単な解説をしながらRAIIというC++で有名なイディオムの理解を深めていきます。
ついでに、例外について簡単な解説をした後、例外安全について触れ、RAIIの利点を紹介します。ただし第1回目は私の都合でムーブコンストラクタまでご紹介します。第2回目で後半を説明します。
さて、初心者にありがちなのですが、ポインタを使いこなそうとしています。残念ながら、実際はポインタをなるべく使わない方向に向けた方がいいのです。そのためにRAIIを活用していきましょう。
コンストラクタとデストラクタ
既にクラスの概要についてはご存じだとは思いますが、クラスにはコンストラクタという特殊なメンバ関数が存在します。これはクラスの初期化をするためのものですが、人によっては次のように書く人がいるみたいです。
class S { public: S() { } void initialize() { // 初期化処理 } };
これはこのクラスを使う人に負担を押しつける書き方なのでやってはいけません。初期化を遅延したければstd::optional (C++17予定)やそれに類するoptional (自作可能)を使用すればいい話です。では何故ダメなのでしょうか?
まずはじめに呼び出す時を考えます。
S s; // コンストラクタが呼ばれる // この間はsは使えない、意味を持たない s.initialize(); // これを呼ばないといけない
このようにコンストラクタを実行した後に無効な状態が出来てしまい、危険です。また、ユーザーは呼び忘れるかも知れません。初期化処理はコンストラクタに書きましょう。
そしてデストラクタですが、後で述べますが、通常はデストラクタはデフォルトのものを使用するべきです。つまり定義しません。あるいはデフォルト宣言します。
- 定義しない
class S { public: S() { } };
- デフォルト
class S { public: S() { } ~S() = default; };
ただし、コンストラクタで何らかのリソース(メモリ、ファイル系、ソケット系、ハンドル系など)を獲得した場合は、デストラクタで解放しなければなりません。後で述べますが、通常はメンバに任せます。
デストラクタが呼ばれるとき
class S { public: S() { } ~S() = default; }; void func() { S s; }
関数func内でクラスSのインスタンスsがローカル変数として宣言されています。このため、関数func内ではスタックにクラスSのインスタンスを積みます。このとき変数sを初期化するためにSのコンストラクタが呼び出されます。そしてこの関数を抜けるときに(returnや例外のthrowあるいは関数の終端)デストラクタが呼び出され、スタックを破棄します。
つまり、ローカル変数のスコープを抜けるときにデストラクタが呼び出されます。
あるいは次のような場合もあります。
void func() { S* s = new S; // コンストラクタが呼ばれる delete s; // デストラクタが呼ばれる }
この例では、関数func内でS型へのポインタS* sがローカル変数として宣言されています。このポインタは64bit環境では8byte(=64bit)の変数ですが、ポインタの指す先(実体)は確保できるどんなサイズでも構いません。実体はnew演算子によって生成され、スタック上にはなく、ヒープ上にあります。そのヒープ上を指し示すポインタをスタックに積んでいます。そして、要らなくなったら明示的にdeleteしなければなりません。ポインタが関数を抜けても、そのポインタが使えなくなるだけで、実体は残ったママなのです。
newすると実体が生成されますから、そのときにコンストラクタが呼ばれます。また、deleteするときにデストラクタが呼ばれて最後にメモリが解放されます。
このような例を出してあれですが、初心者はnew/deleteを一切使わない、これに限ります。ただし今回はこのnew/deleteを使った場合、即ちライブラリを作る側として話を進めますので、ついでに使い方も覚えてしまいましょう。new/deleteを使わないから、new/deleteを閉じ込めるにステップアップです。
RAII
さてリソースについてですが――先ほどはnew/deleteを使ったメモリがそのリソースに相当しますが――先ほどのようにnewとdeleteが分かれていると管理が大変です。次の例を考えましょう。
S* func() { S* s = new S; if (s->xxx()) { delete s; // これ書き忘れそう return nullptr; } return s; // というか誰がdeleteするの? }
クラスSには何らかの条件判断を行うxxxメンバ関数が定義されているとします。この関数が成功したときには0を返し、そうでないときはそれ以外を返します。そして失敗したときはfuncはnullptrを返し、成功したときはnewしたポインタを返すとしましょう。
まず、newしたポインタを関数が返すのは間違っていることは置いておいて、関数から抜けるに当たってその前にdeleteしないといけないのが怪しいです! この例だと1つだけですがこんなのが何個もあったら地獄絵図です。
では、どうしたらいいでしょうか? 関数から抜けたときに、というのがポイントです。そう、先ほどのデストラクタですね。これを利用してみましょう。
class P { public: P() : s { new S } {} S* get() { return s; } ~P() { delete s; } private: S* s; };
まずはこのようなSのラッパークラスを定義します。ラッパーとは、プログラミングでよく出てきますが、あるものを覆い被すようにして使いやすくするものです。上位レイヤーともいいます。ここではクラスPがクラスSのラッパーで有り、今回の主役です。
コンストラクタではSをnewしてメンバ変数S* sに保存します。そしてデストラクタではsをdeleteします。従って、このクラスPのインスタンスはスコープを抜けたときに自動的にdeleteしてくれます。
使ってみましょう。
S* func() { P p; if (p.get()->xxx()) { // delete s; 不要 return nullptr; } return p.get(); // ここでもdeleteされてしまう! }
このように、自動的にdeleteしてくれます。このようにコンストラクタでリソースを獲得し、デストラクタで後処理をする方法をRAIIといいます。ところがこのままでは問題があります。S*を返すときに消されてしまいまうのです! 後で改善してみましょう。
コピーコンストラクタ
コピーコンストラクタは、そのオブジェクトを渡してコピーするのが役目です。後で述べますが、通常はデフォルトにすべきです。
例えば次のクラスを考えます。
class C { public: int x; int y; };
デフォルトでコピーコンストラクタが定義されているので、クラスのコピーが可能です。次のように使います。
C c { 1, 2 }; // cをx=1,y=2で初期化 C c2 { c }; // c2をcをコピーして初期化
デフォルトのコピーコンストラクタだとメンバ(x, y)のコピーを逐一行ってくれます。ただし、メンバがコピー可能出ない場合はその限りではありません!従って、メンバにはコピー可能なものだけを並べる必要があります。
コピーコンストラクタを自分で定義してみましょう。コピーコンストラクタも、いわばコンストラクタです。ただ、条件があって自分自身の参照を引数に取ります。
class C { public: C(C const& it) : x { it.x }, y { it.y } {} int x; int y; };
コンストラクタのように初期化子: メンバ{初期化値}
で初期化します。
ポインタのコピーは危険!
さて本題です。ポインタがある場合です。ポインタをコピーするのは多くの場合誤りです。先ほどのクラスPのなかにポインタがありますね! これをコピーするとどうなるか見てみましょう。 (分かりやすくコピーコンストラクタを定義していますが、この場合はポインタ値はコピーできるので、デフォルトでコピー可能です。)
class P { public: P(S* s) : s { s } {} P(P const& it) : s { it.s } { } S* get() { return s; } ~P() { delete s; } private: S* s; };
これを使った関数funcを見てみましょう。
void func() { P p; P p2 { p }; // pと同じものを指すp2 p.get() == p2.get(); // true } // pとp2のデストラクタが呼び出される
pとp2の中のsは同じポインタを指しています。従って、deleteするとき同じポインタをdeleteしてしまいます!! 2重のdeleteはやってはいけません。選択肢は3つです。
- 新しくSをnewして、中身をコピーする
- コピーを禁止する
- 何らかの機構で2重deleteできないようにする
参考までに標準ライブラリで言うと、1番目がstd::vectorのようなもの。2番目がstd::unique_ptrのようなもの。3番目がstd::shared_ptrのようなものです。自分が必要とするものを選びます。
コピーコンストラクタでリソース確保
ここでは、1番目の新しく確保してコピーを考えてみましょう。
class P { public: P(S* s) : s { s } {} P(P const& it) : s { new S { *it.s } } { } S* get() { return s; } ~P() { delete s; } private: S* s; };
とは言っても簡単で、コピーコンストラクタを改良しただけです。つまり、新しくnewするのですが、その際にコピー元となるit.sの実体である*it.sの参照を渡して、Sのコピーコンストラクタに任せる、というものです。クラスの責任はわけて、SのコピーはSに任せるのです。
こうすることでコピーしても2重deleteされなくて済みます!
void func() { P p; P p2 { p }; // pとは異なるものを指す p.get() == p2.get(); // false } // pとp2のデストラクタが呼び出されるが、大丈夫
こうすることで先ほどの例である次のものが改善されます。
S* func() { P p; if (p.get()->xxx()) { // delete s; 不要 return nullptr; } return p.get(); // ここでもdeleteされてしまう! }
これが、
P func() { P p; if (p.get()->xxx()) { // delete s; 不要 return ???; } return p; // 新たにコピーを作って返す }
と書けます。関数の戻り値では新しくコピーを作って返します。ただし、nullptrのようなものは返せなくなります。対策は3つあります。
- 例外を使ってreturnではなくthrowする。
- Pがnullptrを持てるようにする。
- std::optionalのように無効値を持つクラスでラップする。
このエラーがシステムの正常に稼働する範囲の状態の一つの時は例えばstd::optionalでラップしたり、nullptrを保持できるようにした方が良いでしょう。そうではなく、起こったらユーザーに選択を迫らなければならなかったり、直前のロジックを辞めなければならなかったりするときは例外を使用するのが良い選択です。
P func() { P p; if (p.get()->xxx()) { throw std::exception { "初期化に失敗しました。" }; // 内部のsはdeleteされる。 } return p; // 新たにコピーを作って返す }
これでめでたくSをラップしてメモリ管理をPに任せることが出来るようになりました。ちなみに何度も言いますが、通常はnew/deleteを使う必要がありません。今回はライブラリを作る側の話をしています。
さて早速関数funcから受け取ってみましょう。
int main() { try { P p = func(); // これは代入ではない // pを使う } // 勝手に解放される catch (std::exception const& it) { std::cout << it.what() << std::endl; } }
ポイントは4つです。まずは例外の使い方ですが、tryブロックとcatchブロックに分かれます。tryのなかで例外がthrowされると、catchへ飛びます。このときローカル変数のデストラクタは全て呼び出されます。今回はfuncの中で例外が発生する可能性がありますが、まだpが構築されていないのでpのデストラクタは呼び出されません。
2つめは例外はstd::exception const& itで受け取ることです。throwはどんなデータを渡しても良いのですが、お行儀の用意例外の書き方は、std::exceptionを継承した例外クラスを(必要なら)自分で定義してそれを渡します。これについてはまたいつか述べます。
3つめはもしもfuncが成功したら、pは初期化されます。代入演算子ではなく、これはコピーコンストラクタが呼ばれます。P p = func();
はP p { func() };
と等価です。
4つめは、tryブロックを抜けたらpのデストラクタが呼ばれ、中でnewしたSは自動的にdeleteされます!大事なことなのでもう一度言いますが、tryブロックを抜けたらpのデストラクタが呼ばれ、中でnewしたSは自動的にdeleteされます! (ここ感動するところです。)
無駄を省くムーブコンストラクタ
勘のいい人は気付くかも知れませんが、先ほど定義したクラスPには無駄があります。
P func() { P p; // 中ではここでSをnew if (p.get()->xxx()) { throw std::exception { "初期化に失敗しました。" }; } return p; // 中ではここでもSをnew }
先ほども触れましたが、関数の戻り値にポインタや参照を指定しない場合、クラスをコピーして返します。
P func() {
P p;
return p;
}
は、
P func() {
P p;
return P { p };
}
のような感じになります。つまりコピーされてしまうのです。もちろん、ローカル変数のポインタや参照は絶対に返してはいけませんのでこれはこれで正しいプログラムなのですが、無駄がありますね。newは1回で良いはずなのに……。そんな理由で登場したのがムーブコンストラクタです。
ムーブコンストラクタ
ムーブコンストラクタは定義自体は簡単で、P&&を引数に取るコンストラクタです。次のようになります。
class P { public: P() : s { new S } {} P(P const& it) : s { new S { *it.s } } { } P(P&& it) { } // ムーブコンストラクタ S* get() { return s; } ~P() { delete s; } private: S* s; };
このコンストラクタが何か特別なことをしてくれるかというとそういうわけではありません。この中に、ムーブする処理を書かないといけないのです。とは言っても簡単です。
要は、新しくnewしてコピーをするのではなく、ポインタを直接コピーしてしまえばいいんです! え? それじゃあ最初の2重deleteと同じじゃないかって? そうなのです。コピーコンストラクタだと、さきほどのように2重delete問題が起こってしまうのです。
class P { public: P(S* s) : s { s } {} P(P const& it) : s { it.s } { } // itは書き換えられないなあ S* get() { return s; } ~P() { delete s; } private: S* s; };
ところが、ムーブコンストラクタの引数P&& (2回&が付いてますが間違ってません!)はconstじゃないので、itを書き換えられます。そして! 引数itのit.sをnullptrにしてあげることで、2重deleteが防がれるのです!
class P { public: P() : s { new S } {} P(P const& it) : s { new S { *it.s } } { } P(P&& it) : s { it.s } { // ポインタをコピー it.s = nullptr; // でも相手のポインタはnullptrにしちゃう } S* get() { return s; } ~P() { delete s; } private: S* s; };
これじゃあムーブ元のポインタが無効になっちゃうじゃないか! とお思いでしょうが、それでいいのです。ムーブ=移動ですからね。自分は死ぬ代わりに、相手にポインタを託す、そんなものです。
呼び出す側について考えてみましょう。要はムーブしたいときは、コピーとは違って、P&&型にキャストすれば良いのです。
P func() {
P p;
P p2 { (P&&)p }; // ムーブされる
}
もちろん、いくら初心者でもこのような書き方はC++を書くものとして許されません。static_castを使って、
P func() { P p; P p2 { static_cast<P&&>(p) }; // ムーブされる }
と書きます……が、実は&&にキャスとしてくれる便利な関数が標準で用意されています。
P func() { P p; P p2 { std::move(p) }; }
ムーブしているのが見た目に分かりやすいですね。当然、pはムーブされたのでpの中のsはnullptrになっています。そして、pのデストラクタではdelete nullptr;
が実行されるわけですが、deleteはnullptrを渡してもよく、その場合は何も起こらないので正しい挙動です。
むしろ重要なのは、ムーブ後のデータは、デストラクタで正しく、それもなるべく高速に解体されることが望まれます。この場合は相手のポインタをnullptrにすることで、2重deleteされず(正しく)、高速に(nullptrチェックも要らずに)解体されるのです。
右辺値参照
ところでこの&&は何物かというと、右辺値参照と言います。この辺の話は話し始めると長くなるので省略しますが、簡単に言うと=の左に置けるものが左辺値で、それ以外が右辺値です! 右辺値をコンストラクタに渡すとムーブしてくれます。そして、右辺値にするための便利な関数がstd::moveです。
さて、話を戻して次の例を見ましょう。
P func() { P p; // 中ではここでSをnew if (p.get()->xxx()) { throw std::exception { "初期化に失敗しました。" }; } return std::move(p); // ムーブするからnewしない }
最後のstd::move(p)に注目ですが、実はこのstd::moveは要りません。なぜならばreturn後はローカル変数pは不要なのでmoveしても何も問題が無いからです。仕様で右辺値扱いされます。つまり、こう書けます。
P func() { P p; // 中ではここでSをnew if (p.get()->xxx()) { throw std::exception { "初期化に失敗しました。" }; } return p; // ムーブしてくれる }
実はこのように何も書かないとコンパイラが最適化してムーブすらしない場合があります。つまり、コンストラクタのみ実行され、それが利用されるのです。これは自分で調べてみて下さい。(キーワード : RVO, NRVO)
メモリプールの修正
インナークラスを使って定義すると、その内部のクラスを呼び出すときに、あるクラスがそのクラスのポインタを保持しようとすると実体化できないとエラーになるので修正しました。
namespace mytools { template <typename T, size_t SIZE> class Pool { protected: template <typename T> class Storage { public: Storage() : next { nullptr } {} T& operator[](int no) { return *reinterpret_cast<T*>(&buffer[no]); } Storage* makeNext() { next = new Storage {}; return next; } ~Storage() { delete next; } private: std::aligned_storage_t<sizeof(T)> buffer[SIZE]; Storage* next; }; template <typename T> class StorageManager { public: StorageManager() : position { 0 } { last_storage = &storage; } template <typename ...Args> T* make(Args&& ...args) { T* ret; if (free_list.empty()) { ret = &(*last_storage)[position]; ++position; if (position >= SIZE) { last_storage = storage.makeNext(); position = 0; } new(ret)T { std::forward<Args>(args)... }; } else { ret = free_list.top(); new(ret)T { std::forward<Args>(args)... }; free_list.pop(); } return ret; } void free(T* it) { it->~T(); free_list.push(it); } private: Storage<T> storage; Storage<T>* last_storage; std::stack<T*> free_list; size_t position; }; }; template <typename T, size_t SIZE> class SharedPool; template <typename T, size_t SIZE> class DataAndReference; template <typename T, size_t SIZE> class Reference { public: Reference() : counter { 1 } {} void increment() { ++counter; } void decrement() { --counter; } void release(SharedPool<T, SIZE>* pool, DataAndReference<T, SIZE>* pointer) { if (counter == 0) { pool->free(pointer); } } private: size_t counter; }; template <typename T, size_t SIZE> class DataAndReference { public: template <typename... Ts> DataAndReference(Reference<T, SIZE> reference, Ts&&... ts) : reference { reference } { new (&data) T { std::forward<Ts>(ts)... }; } DataAndReference() {} T& operator*() { return *this->get(); } T const& operator*() const { return *this->get(); } T* operator->() { return this->get(); } T const* operator->() const { return this->get(); } T* get() { return static_cast<T*>(static_cast<void*>(&data)); } T const* get() const { return static_cast<T const*>(static_cast<void*>(&data)); } ~DataAndReference() { static_cast<T*>(static_cast<void*>(&data))->~T(); } Reference<T, SIZE> reference; std::aligned_storage_t<sizeof(T)> data; }; template <typename T, size_t SIZE = 128> class SharedPtr { public: SharedPtr(SharedPool<T, SIZE>* pool, DataAndReference<T, SIZE>* dar) : pool { pool }, dar { dar } { } SharedPtr() : pool { nullptr }, dar { nullptr } {} SharedPtr(SharedPtr const& it) : pool { it.pool }, dar { it.dar } { dar->reference.increment(); } SharedPtr(SharedPtr&& it) : pool { it.pool }, dar { it.dar } { it.dar = nullptr; } SharedPtr& operator=(SharedPtr const& it) { this->~SharedPtr(); return *new(this) SharedPtr { it }; } SharedPtr& operator=(SharedPtr&& it) { this->~SharedPtr(); return *new(this) SharedPtr { std::forward<SharedPtr>(it) }; } ~SharedPtr() { if (dar != nullptr) { dar->reference.decrement(); dar->reference.release(pool, dar); } } T& operator*() { return *dar->get(); } T const& operator*() const { return *dar->get(); } T* operator->() { return dar->get(); } T const* operator->() const { return dar->get(); } T* data() { return dar->get(); } T const* data() const { return dar->get(); } private: SharedPool<T, SIZE>* pool; DataAndReference<T, SIZE>* dar; }; template <typename T, size_t SIZE = 128> class SharedPool : Pool<T, SIZE> { public: using SharedPtr = SharedPtr<T, SIZE>; template <typename ...Args> auto makeShared(Args&& ...args) { return SharedPtr { this, make(std::forward<Args>(args)...) }; } template <typename ...Args> DataAndReference<T, SIZE>* make(Args&& ...args) { return storage_manager.make(Reference<T, SIZE> { }, std::forward<Args>(args)...); } void free(DataAndReference<T, SIZE>* it) { storage_manager.free(it); } private: StorageManager<DataAndReference<T, SIZE>> storage_manager; }; template <typename T, size_t SIZE> class UniquePool; template <typename T, size_t SIZE = 128> class UniquePtr { public: UniquePtr(UniquePool<T, SIZE>* pool, T* pointer) : pool { pool }, pointer { pointer } { } UniquePtr() : UniquePtr { nullptr, nullptr } {} UniquePtr(UniquePtr const& it) = delete; UniquePtr(UniquePtr&& it) : pool { it.pool }, pointer { it.pointer } { it.pointer = nullptr; } UniquePtr& operator=(UniquePtr const& it) = delete; UniquePtr& operator=(UniquePtr&& it) { this->~UniquePtr(); return *new(this) UniquePtr { std::move(it) }; } ~UniquePtr() { if (pointer != nullptr) { pool->free(pointer); } } T& operator*() { return *pointer; } T const& operator*() const { return *pointer; } T* operator->() { return pointer; } T const* operator->() const { return pointer; } T* data() { return pointer; } T const* data() const { return pointer; } private: UniquePool<T, SIZE>* pool; T* pointer; }; template <typename T, size_t SIZE = 128> class UniquePool : Pool<T, SIZE> { public: using UniquePtr = UniquePtr<T, SIZE>; template <typename ...Args> auto makeUnique(Args&& ...args) { return UniquePtr { this, make(std::forward<Args>(args)...) }; } template <typename ...Args> T* make(Args&& ...args) { return storage_manager.make(std::forward<Args>(args)...); } void free(T* it) { storage_manager.free(it); } private: StorageManager<T> storage_manager; }; }
C++における契約プログラミング♪
契約プログラミング
ある程度の規模のプログラムを書くと、関数が増えてきます。ある関数に何らかのデータを渡し、処理して結果を返したり、あるいはクラスの状態が変わったりします。ところで、プログラム中の実行時エラーには2種類あります。
- プログラマのミスによる実行時エラー (誤った計算によるバグ、メモリのバグ、想定しない状態によるバグ)
- ユーザーのシステムによる実行時エラー (リソース不足、ユーザーの誤った入力)
通常、前者はデバッグ中にのみ検出を行い、リリース版では後者のみ判断を行います。全てのデバッグが終わり、全てのプログラムがプログラマの意図するとおりに動くことを確認したらリリース版で動かすのです。
契約プログラミングによる設計では、このようなプログラマのミスによる実行時エラーを検出するために、プログラムソース中にそのような条件を記述することにより、デバッグ時に確実に条件を守らせる = 最低限のテストを盛り込むことができます。
契約プログラミングにおける条件は、
- 事前条件 : 関数の開始時に満たすべき条件
- 事後条件 : 関数の終了時に満たすべき条件
- 不変条件 : 外部に公開されるオブジェクトが常に満たすべき条件
があります。
assertマクロを使えば、デバッグ時に有効な条件を記述でき、リリース時には消し去ることが出来ます。ところがこのマクロだけだと、事後条件や不変条件は書きづらくなり、見た目にもわかりにくくなります。そこで独自のマクロとクラスを定義することで契約プログラミングがしやすくなるようにしました。
assertマクロ
ある関数funcはint型の引数1つを受け取ってint型の値を返すとします。そしてこの関数の引数には0以上の整数が与えられていると仮定しています。更に内部の計算によって出力は常に0以上となると仮定します。
int func(int n) { // nは0以上の時のみ動作する関数である // ~~処理~~ // 処理した後の結果retは常に0以上になるはずである return ret; }
この関数を呼び出す側は常に0以上で呼び出す必要があるわけです。このようなチェックは通常関数内には書きません。なぜならば、呼び出す側が0以上と保証している場合、無駄な処理(if文)が走ってしまうからです。このような前提を事前条件と言い、関数の責任ではなく呼び出し側の責任となります。
とはいえ、事前条件は文字で書くだけだとデバッグ時に誤って0未満の数を渡してしまう可能性があります。(呼び出し側が常に確認しているとは限りません。なぜならば、もしも理想のプログラムを書いたときには事前条件を省略できる可能性があるからです。) 契約プログラミングでは、コメントではなく、プログラム中に条件を書きます。
例えばassertマクロがあります。これはデバッグ時のみ有効で、
assert(n >= 0);
と書いて、常にn >= 0となるはずだ、と表明しているわけです。
提案手法
使い方
例えば次のような意味の無いクラスTestがあったとします。
class Test { public: // nは0以上 Test(int n) : n { n } { } // mは0以上 void test(int m) { --m; n -= m; } private: int n; // nは常に0以上 };
このとき、コンストラクタは、
- 事前条件 : n >= 0
そしてtestメンバ関数は、
- 事前条件 : m >= 0
- 事後条件 : m >= 0
- 不変条件 : n >= 0
です。これらをプログラム中に書くマクロを定義した上で、次のように書きます。
class Test { public: Test(int n) : n { n } { CREQUIRE(n >= 0); // 事前条件 } // 不変条件 CINVARIANT({ CREQUIRE(n >= 0); }); void test(int m) { // 事前条件 CREQUIRE(m >= 0); // 事後条件 CENSURE({ CREQUIRE(m >= 0); CCHECK(); // 不変条件のチェック }); --m; n -= m; } private: int n; };
実装
- contract.h
#pragma once #include <cassert> #include <string> #define CREQUIRE(condition) assert(condition, #condition); #ifdef NDEBUG #define CENSURE(condition) (nullptr) #else #define CENSURE(condition) auto _debug_contract_ = Contract::makePostCondition([&](){ condition }); #endif #ifdef NDEBUG #define CINVARIANT(condition) (nullptr) #define CCHECK() (nullptr) #else #define CINVARIANT(condition) void _ContractCheckInvariant() const { condition; } #define CCHECK() (this->_ContractCheckInvariant()) #endif namespace Contract { template <typename F> class PostCondition { public: PostCondition(F condition) : condition { condition } { } ~PostCondition() { condition(); } F condition; }; template <typename F> static PostCondition<F> makePostCondition(F&& condition) { return PostCondition<F> { std::forward<F>(condition) }; } }
C++14でcurry化
関数型言語では関数が第一級の値という。私もよく分かっていないが、とりあえず何をするにも関数で、関数を操作するのだそうだ。その一つの例としてカリー化があげられ、ある関数に対して引数を一部与えて束縛し、その関数を返すというものである。C++で実装してみよう。
要するに、curry(関数)としてあげると、関数は「ある引数args1を取り〈残る引数args2を取る関数〉を返す関数」であると分かる。内部で関数を生成するためにジェネリックラムダを用いた。これはC++14になって導入されたもので、
auto l = [](auto a) {};
として使える。このラムダ式lにはどのような型も入れられる。つまりtemplateが使えると言うことである。
あとはC++11からあるお馴染みのキャプチャに関数を入れてあげれば無事curryのできあがり。
template <typename F> auto curry(F f) { return [f](auto... args) { return [f, args](auto... args2) { return f(args..., args2...); }; }; }
ただこれだと問題があって、例えば引数に参照が入っているような関数では使えない。それはテンプレートでも同じ問題で、
template <typename T> void func(T t) { }
とすると、t自体が参照ではなくなってしまうからだ。この問題を解決するために完全転送を行う。使い方自体は単純で、TをT&&にして、tをstd::forward
auto curry(F&& f) { return [&f](auto&&... args) { return [&f, &args](auto&&... args2) { return f(std::forward<decltype(args)>(args)..., std::forward<decltype(args2)>(args2)...); }; }; }
と思ったがこれでも完全ではない。というのも、元の関数がもしも参照を返すような関数の時は戻り値から参照が取られてしまう。従って、関数fの戻り値と一致させるためにdecltype(auto)を入れてあげる必要がある。autoだけでは参照が取れてしまうのだ。
auto curry(F&& f) { return [&f](auto&&... args) { return [&f, &args](auto&&... args2) -> decltype(auto) { return f(std::forward<decltype(args)>(args)..., std::forward<decltype(args2)>(args2)...); }; }; }
使い方
auto l = [](int a, int b) { return a + b; }; auto f = curry(l)(10); auto v = f(5);
fはカリー化された関数で、足し算を定義してあるl及び、それをカリー化して第1引数に10を入れたものである。fもまた関数で引数を1つ持つ関数である。従って、v = 10 + 5が実行される。releaseモードで実行すればこの程度のコードなら簡単に展開されてしまう。すなわち実質10+5が実行されるに過ぎない。このカリー化はtemplateを使うので最適化がかかりやすく、実行速度が出ると思われるのでもしも有用な使い方があれば積極的に使っていきたい。ただし実用性のほどはよく分からない。
C#のLINQライクな遅延評価ライブラリを作ったので、そのときのwhereやmapに入れる関数として引数を減らしつつ柔軟性を持たせるようなコードに使えるかも知れない。
余談
実は完全転送版とそうでないものだとVisual Studio 2017 RCでは最適化の効きが異なるようだ。アセンブリコードを見ると余分なコードがのっかって+50%遅くなる。原因は分からないが、改善されることを祈る。
int main() { auto startTime = std::chrono::system_clock::now(); const int n = 1000000000; int s = 0; for (int i = 0; i < n; ++i) { auto f = func_curry(i); auto v = f(i); s += v; } auto endTime = std::chrono::system_clock::now(); auto timeSpan = endTime - startTime; std::cout << s << std::endl; std::cout << "処理時間:" << std::chrono::duration_cast<std::chrono::milliseconds>(timeSpan).count() << "[ms]" << std::endl; auto l = [](int a, int b) { return a + b; }; startTime = std::chrono::system_clock::now(); s = 0; auto f = curry(l); for (int i = 0; i < n; ++i) { auto c = f(i); auto v = c(i); s += v; } endTime = std::chrono::system_clock::now(); timeSpan = endTime - startTime; std::cout << s << std::endl; std::cout << "処理時間:" << std::chrono::duration_cast<std::chrono::milliseconds>(timeSpan).count() << "[ms]" << std::endl; system("pause"); return 0; }
メモリプール
概要
- 固定長メモリプールを再び作った。
- 簡易unique_ptr, shared_ptrを自作し、速度の向上を図った。
- 普通にunique_ptr, shared_ptrを使用するよりも2~3倍の実行速度が得られた。
実装について
簡易shared_ptrであるSharedPtrを使用することを前提としたメモリプールSharedPoolと、共有を前提としない、UniquePoolを用意した。それぞれは共通部分のPoolを基底としているがポリも―フィックな動作をしないのでvirtualにしていない。また、SharedPoolはその特性上、参照カウンタ及び親のポインタを保存するためのメモリを余分に確保していることに注意されたい。一方でUniquePoolはメモリ上のオーバーヘッドはないが、UniquePtrを使用するためのオーバーヘッドが幾分かある。
簡単に言うと、大きなメモリブロックをリンクリストでつないでそこから切り出し、使わなくなったメモリはstd::stackにいれて再利用をしている。ページの減少は実装していない。参照カウンタ方式はPointerクラスとしてその実態と参照カウンタを合わせてこれを単位とする。メモリの切り出し1回につき参照カウンタも一緒に生成するためコストの低減に繋がる。
使用例
mytools::SharedPool<C> pool; // (1) using sptr = decltype(pool)::SharedPtr; // (2) std::array<sptr, 32> list2; // (3) for (int i = 0; i < 100; ++i) { auto s = pool.make_shared(i); // (4) list2[(何らかの乱数)] = std::move(s); // (5) }
まず、(1)でひとまとまりのメモリを確保する。初期値は128個であるが、テンプレート引数に入れることで可変に出来る。なお、今回の実験ではこれを変えたところであまり優位な差は得られなかった。(2)では簡易スマートポインタの型を取得している。各プールごとに型が異なるので、必要ならばこのようにする必要がある。(3)では32個のスマートポインタを保存する配列を作っている。今回はこの配列に生成したデータをランダムに入れて検証している。この例では(4)でデータを生成し、(5)でランダムに配列に突っ込む。既にある場合は参照カウントが0になって消滅し、新しいオブジェクトが入ることになる。従って高々32個のオブジェクトしか存在し得ないことになる。
ベンチマーク結果
- Visual Studio 2017 RC Release (--std:C++ latest)で実験を行った。
- std::shared_ptrのみ : 163[ms]
- メモリプール&SharedPtr : 58[ms]
このようにメモリプールを使用すると確保の時間が低減される。大量のオブジェクトを使い回すようなプログラム、すなわち個数の上限はある程度決まっているが、寿命が異なる場合などには有用だと思われる。なお、スマートポインタを使用しないでnew-delete/メモリプールの比較、unique_ptr/メモリプール&UniquePtrの比較も行ったが、いずれも当たり前なのだがメモリプール仕様の方が2~3倍高速に動いた。
class C { public: C(int n) : n { n } { } ~C() { } int buffer[256]; int n; }; int main() { constexpr int n = 64; std::random_device rd; std::mt19937 mt(rd()); std::uniform_int<> ui { 0, n - 1 }; const int N = 1000000; auto start = std::chrono::system_clock::now(); std::array<std::shared_ptr<C>, n> list; for (int i = 0; i < N; ++i) { auto s = std::make_unique<C>(i); list[ui(mt)] = std::move(s); } auto end = std::chrono::system_clock::now(); std::cout << "1:" << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << "[ms]" << std::endl; mytools::SharedPool<C> pool; using sptr = decltype(pool)::SharedPtr; start = std::chrono::system_clock::now(); std::array<sptr, n> list2; for (int i = 0; i < N; ++i) { auto s = pool.make_shared(i); list2[ui(mt)] = std::move(s); } end = std::chrono::system_clock::now(); std::cout << "2:" << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << "[ms]" << std::endl; system("pause"); return 0; }
実装
#pragma once #include <stack> #include <memory> namespace mytools { template <typename T, size_t SIZE = 128> class Pool { protected: template <typename T> struct Storage { Storage() : next { nullptr } {} T& operator[](int no) { return (reinterpret_cast<T*>(static_cast<void*>(buffer)))[no]; } Storage* makeNext() { next = new Storage {}; return next; } ~Storage() { delete next; } private: char buffer[SIZE * sizeof(T)]; Storage* next; }; template <typename T> class StorageManager { public: StorageManager() : position { 0 } { last_storage = &storage; } template <typename ...Args> T* make(Args&& ...args) { T* ret; if (free_list.empty()) { ret = &(*last_storage)[position]; ++position; if (position >= SIZE) { last_storage = storage.makeNext(); position = 0; } new(ret)T { std::forward<Args>(args)... }; } else { ret = free_list.top(); new(ret)T { std::forward<Args>(args)... }; free_list.pop(); } return ret; } void free(T* it) { it->~T(); free_list.push(it); } private: Storage<T> storage; Storage<T>* last_storage; std::stack<T*> free_list; size_t position; }; }; template <typename T, size_t SIZE = 128> class SharedPool : Pool<T, SIZE> { public: class Pointer; private: class Reference { public: Reference(SharedPool* pool) : counter { 1 }, pool { pool } { } void increment() { ++counter; } void decrement() { --counter; } void release(Pointer* pointer) { if (counter == 0) { pool->free(pointer); } } private: size_t counter; SharedPool* pool; }; public: class Pointer { public: template <typename... Args> Pointer(SharedPool* pool, Args&& ...args) : t { std::forward<Args>(args)... }, reference { pool } {} T& operator*() { return t; } T const& operator*() const { return t; } T* operator->() { return &t; } T const* operator->() const { return &t; } T t; Reference reference; }; class SharedPtr { public: SharedPtr(Pointer* pointer) : pointer { pointer } { } SharedPtr() : SharedPtr { nullptr } {} SharedPtr(SharedPtr const& it) : pointer { it.pointer } { it.~SharedPtr(); pointer->reference.increment(); } SharedPtr(SharedPtr&& it) : SharedPtr { it } { it.~SharedPtr(); it.pointer = nullptr; } SharedPtr& operator=(SharedPtr const& it) { return *new(this) SharedPtr { it }; } SharedPtr& operator=(SharedPtr&& it) { return *new(this) SharedPtr { std::forward<SharedPtr>(it) }; } ~SharedPtr() { if (pointer != nullptr) { pointer->reference.decrement(); pointer->reference.release(pointer); } } T& operator*() { return pointer->t; } T const& operator*() const { return pointer->t; } T* operator->() { return &pointer->t; } T const* operator->() const { return &pointer->t; } T* data() { return &pointer->t; } T const* data() const { return &pointer->t; } private: Pointer* pointer; }; template <typename ...Args> auto make_shared(Args&& ...args) { Pointer* pointer = make(std::forward<Args>(args)...); return SharedPtr { pointer }; } template <typename ...Args> Pointer* make(Args&& ...args) { return storage_manager.make(this, std::forward<Args>(args)...); } void free(Pointer* it) { storage_manager.free(it); } private: StorageManager<Pointer> storage_manager; }; template <typename T, size_t SIZE = 128> class UniquePool : Pool<T, SIZE> { class UniquePtr { public: UniquePtr(T* pointer, Pool* pool) : pointer { pointer }, pool { pool } { } UniquePtr() : UniquePtr { nullptr, nullptr } {} UniquePtr(UniquePtr const& it) = delete; UniquePtr(UniquePtr&& it) : pointer { it.pointer }, pool { it.pool } { it.~UniquePtr(); it.pointer = nullptr; } UniquePtr& operator=(UniquePtr const& it) = delete; UniquePtr& operator=(UniquePtr&& it) { return *new(this) UniquePtr { std::forward<UniquePtr>(it) }; } ~UniquePtr() { if (pointer != nullptr) { pool->free(pointer); } } T& operator*() { return *pointer; } T const& operator*() const { return *pointer; } T* operator->() { return pointer; } T const* operator->() const { return pointer; } T* data() { return pointer; } T const* data() const { return pointer; } private: T* pointer; Pool* pool; }; template <typename ...Args> auto make_unique(Args&& ...args) { return UniquePtr { make(std::forward<Args>(args)...), this }; } template <typename ...Args> T* make(Args&& ...args) { return storage_manager.make(std::forward<Args>(args)...); } void free(T* it) { storage_manager.free(it); } private: StorageManager<T> storage_manager; }; }
コールバックのインターフェース(テンプレート使用)
あるクラスのメンバ関数をコールバックとするにはいろいろな実装があると思いますが、例えばテンプレートを使うと次のようになります。ただし、あんまり美しくないですね……。std::functionalとstd::bindを使えばすぐに終わりますがちょっと速度が出ない問題があります。
#include <iostream> template <typename CallbackClass> class ACallbackInterface { public: ACallbackInterface(void (CallbackClass::* member)(void), CallbackClass* callback_class) : member { member }, callback_class { callback_class } {} void operator()() { (callback_class->*member)(); } private: void (CallbackClass::* member)(void); CallbackClass* callback_class; }; template <typename T> class A { public: A(ACallbackInterface<T>& i) : i { i } { } void func() { i(); } private: ACallbackInterface<T>& i; }; int main() { class CallBack : public ACallbackInterface<CallBack> { public: CallBack() : ACallbackInterface<CallBack> { &CallBack::callback, this } {} void callback() { printf("hello\n"); } }; CallBack call_back {}; A<CallBack> a { call_back }; a.func(); }
ネタの蛇足的解説
本記事はhttps://twitter.com/staryoshi/status/669763731610431488のたくさんの反響を記念して蛇足的な解説を並べたものです。あまりC++に興味を持っていなかった人もこれを見て興味を持っていただけたら幸いです。もちろん、実際にはこれらの表記で困ることはないのですが、こんなこともできてしまうという一種の例でもあります。
このコラの目的は、同一の識別子を用いて関数呼び出しに見えるが実際の定義の仕方は異なるというものはどのくらいあるのか、というのが発端です。ちなみにアイディは宇月まりな on Twitter: "僕が古文を嫌う理由 https://t.co/XiONjbFDYb"より。全く外見は同じだが中身が違う……C++だ、と思って作りました。
当然、いかに目をこらしても文字に違いはありませんので文脈から読み取るしかありません。
その前に
今回、C++啓蒙のためのネタとして投稿したわけですが、どうもC++の魅力が伝えられなかったみたいで大変残念です。
普通に書いていればマクロ名を小文字で書くこともなければ、ローカル変数に代入した変数名と関数名がかぶって混乱することもありません。また、知らなければラムダも関数オブジェクトもマクロもクラスも使わないで書くことだってできます。使わない機能は使わなければいいのです。
C++は使いたい機能だけ使うことができますし、そうすれば自然にC言語に近くなってきます。ちょっと便利なC言語のように使ってもいいと思いますし、多くの人がそうしているでしょう。
今回のネタを通してC++が複雑奇怪だから触るのはやめよう、という思った方は他の最近の言語でも実は多くの機能を知らないのではないでしょうか。他の言語もだいたい似たようなことやそれ以上のことができますが、それを使わない選択も用意されています。
修得するためには結局は1つ1つ丁寧に定義とその存在意義を把握して実際に使ってみるしかないのです。また、修得しなくても代替方法はいくらでも用意されています。ただ、どうしてそんなものがあるかと言うことを改めて考えることでより表現力が広がり、(コーディングの)効率が上がるのではないかと思われます。
あとよく言われるようにJavaScriptのthisも大概です() perlやPHPもアレだし、Rubyも闇。Haskellは(初見で)訳分からないし、Rustは糞難しいし、どこに行っても闇しかないんです……。
各コマのソースコード例
1コマ目(関数呼び出し)
C++関数とは、ある引数を取ってそれ(とまあいろいろな情報)を元にある結果を返すことのできる処理のまとまりです。引数には0個以上を指定します。つまりない場合もあります。
void name() { } // 関数定義 int main() { name(); // 関数呼び出し }
namespace内でも広域内でもはたまたstatic, constexpr, inlineをつけても基本的には関数です。
void name(); // グローバル宣言 void name() { } // グローバル定義 namespace { void name(); // 名前空間内宣言 void name() { } // 名前空間内定義 }
2コマ目(マクロ呼び出し)
マクロとはC++のソースコード上にある識別子を別の識別子に置き換えてしまう黒魔法です。0個以上の引数を取る置換列により置換できます。
#define name() std::cout<<"empty\n" int main() { name(); // マクロ呼び出し }
3コマ目(メンバ関数呼び出し)
C++にはクラスを定義できます。クラスには関数を持つことができ、通常はクラスを直接操作する関数です。ただしメンバ関数と上記の関数とは扱いが異なります。ただし、staticをつけると上記の関数とほぼ同じ扱いになります。定義は同じく{}をつけるだけです。
class C { public: C() { name(); // クラス内のメンバ関数呼び出し this->name(); // これはthis->を省略していない場合 } void name(); // メンバ関数の宣言 static void name(); // 静的メンバ関数の宣言 }; int main() { C c; }
4コマ目(関数ポインタやラムダ式の関数呼び出し)
関数ポインタは関数のアドレスを別の変数Xに保持し、X経由でその関数を呼び出すものです。
ラムダ式とは、端的にいえば関数を関数内でかけるようにして、さらに他の機能も加えて便利にしたものです。ラムダ式は関数オブジェクトと考えられますが実際にはクロージャ型という異なるものです。というのも関数オブジェクトと定義してしまうとコンパイラが最適化できない場合があるためです。
この2つは実は違う動作がなされるのですが、キャプチャしなければ同じ扱いなので一緒にしてしまいました。
void func() { } // 何らかの関数があって int main() { using func_type = void(*)(); // 関数ポインタの型宣言 func_type name = func; // 関数ポインタの代入 name(); // 変数経由で読み出し (*name)(); // こちらの書き方も可能 }
キャプチャのないラムダ式も代入できます。キャプチャとは、ラムダ式の宣言されたスコープで見える変数を、ラムダ式内で利用することです。何もキャプチャしないときはからのラムダ導入子[]
を指定して[]() { ... }
と書きます。この場合、実質関数ポインタとなります。
int main() { auto name = []() { /* 処理 */ }; name(); // ラムダ式の実行 }
ちなみにキャプチャすると関数ポインタではなくクロージャ型です。以下の例ではラムダ式の外の変数xをキャプチャし、ラムダ式内で変更します。これ以上は本題から逸れるのでやめておきます。興味がありましたらC++で遊んでみると楽しいですよ!
int main() { int x = 100; auto name = [&]() { x += 10; }; // クロージャ型 name(); // 実行 x; // x = 110 }
5コマ目(コンストラクタ呼び出し)
コンストラクタはクラスを初期化する特別な関数です。コンストラクタはメンバ関数とは違ってクラス名と同じにしなければいけません。ちなみにスコープから抜けたりオブジェクトが破棄されたりするときに呼び出されるデストラクタは~をつけて明示的に呼び出せます。今回は~でも入っていたら分かってしまうので除外しましたが。
class name { public: name(); // コンストラクタ ~name(); // デストラクタ };
int main() { name(); // コンストラクタの実行 }
7コマ目(関数呼び出し演算子)
クラスには、そのクラスのインスタンスに直接()をつけて関数呼び出しのように振る舞える関数呼び出し演算子を定義できます。このようなクラスを関数オブジェクトと呼び、この発想を元に簡略記法のラムダ式が提案されました。C++では標準ライブラリやその他のインターフェースとして基本的な知識となります。*1
class C { public: C(); void operator()() { } // 関数呼び出し演算子のオーバーロード }; int main() { C name; name(); // インスタンス名に()をつけて呼び出せる return 0; }
8コマ目(暗黙の型変換による呼び出し)
たぶんC++を知っている人でも漫画内の短い文では推測が困難、あるいは説明不足故に想像がつかなかった人がいるかも知れません。C++では型は暗黙に(=知らないうちに)変換されることがあります。今回はこれを利用しました。
void func() { } // ある関数 class C { public: using func_type = void(*)(); // 変換演算子の定義 operator func_type() { return func; // 関数ポインタを返す } }; int main() { C name; name(); // nameは暗黙の変換によってvoid(*)()にされ、呼び出される return 0; }
この暗黙の変換はやっかいな結果を生むこともあります。そこでこれを抑制するには、explicitをつけます。これを指定することにより、型変換が明示的あるいはコンストラクタに入れられるときのみに行われます。
explicit operator func_type() { return func; // 関数ポインタを返す }
このexplicitをつけた変換演算子が呼び出されるのは、コンストラクタの直接初期化(=のない初期化)かあるいは明示的にキャストした場合に限られます。
次の例ではクラスUがクラスCに変換するexplicit変換演算子を定義しています。クラスCへ暗黙の変換がされるのは1つだけです。すなわち、コンストラクタで直接初期化する場合です。
class C { public: C() {} C(C const& it) { } C& operator=(C const& it) { return *this; } }; class U { public: explicit operator C() { return C(); } }; int main() { U u; C c(u); // ○ Cを直接初期化しているため暗黙変換される c = u; // × UからCへ暗黙変換されないため代入できない C c2 = u; // × コピー初期化では上の直接初期化のようにはいかない }
ちなみによく使われるif文等の中に入れるexplicit bool()
はOKです。これはif分の中の条件は暗黙裏に変換されることが約束されているためです。
蛇足ですが有用な使い方を示しておしまいにしておきます。
class Even { public: Even(int n) : n(n) {} explicit operator bool() { return n % 2 == 0; } private: int n; }; int main() { if (Even(10)) { printf("偶数\n"); } else { printf("奇数\n"); } }