HatoholにおけるC++11の活用
はじめに
Hatoholの開発がはじまってしばらくは、C++11の機能を使用していませんでした。主たるサポートOSの一つであるCentOS6の標準のGCC(g++)では、C++11サポートの対応度が十分ではなかったからです。
現在の開発コミュニティの公式サポートOSは、CentOS 7とUbuntu 14.04 LTSであり、どちらもC++11の多くの機能がサポートされたg++ 4.8が使用できます。ですので、昨年から、簡潔でパワフルな記述が可能なC++11の使用を始めました。また、CentOS 6に関しても、公式サポート対象ではないものの可能なかぎりビルドできる状態を維持したいので、devtoolset-2リポジトリを利用してg++4.8で構築できることを確認しています。
次節以降、よく使用される機能を紹介していきます。
Range based for
データコンテナを直接for文のパラメータとして与えて、要素ごとにブロックを実行する機能。PythonなどのLL (Lightweight Language)では多用さてますね。
従来は、以下のように必要な記述が多く煩雑でした。なお、イテレータitをfor文の中で宣言しないのは、for文の途中で折り返しが発生して見難くなるのを避けているからです。(見やすさについては、主観なので他の意見もあるでしょう)
EventInfoListConstIterator it = events.begin();
for (; it != events.end(); ++it) {
const EventInfo &event = *it;
foo(event);
}
これが次のようにすっきりした記述になりました。
for (const EventInfo &event: events) {
foo(event);
}
型推論
型を自動的に決定する機能で、intなど具体的な型の代わりにautoキーワードを使います。上の例にautoを使うと、次のようにさらに簡潔になります。
for (const auto &event: events) {
foo(event);
}
また、Hatoholではイテレータを前節の例のように長い名前でtypdefすることが多いので、イテレータを宣言するときにも活用しています。前節の最初の例にautoを適用すると、次のように、より簡単になるのが分かるでしょう。
auto it = events.cbegin();
別の例ですが、次のような使い方もしています。かなり、LLぽいですよね。
for (auto label : {"previous", "current"}) {
ラムダ関数
Hatoholで比較的よく使われているのは、関数内関数を定義する場合です。C++11より前だと、関数内関数を構造体やクラスを用いて擬似的に実現できますが、次のような問題点がありました。
- インデントが深くなる
- 構造体内の関数から、外側の関数の変数にアクセスできない
- インスタンスの変数名、または、static関数なら構造体名をつけての呼び出しが必要で冗長
以前のよくあるコードの例がこちらです。関数内関数で、必要な変数はすべて引数で渡すか、メンバとして保持する必要があります。
MyClas::func(const EventInfo &event) {
struct Foo {
static void doIt(const EventInfo &event, MyClass *obj) {
obj->hoo(event);
...
}
};
Foo::doIt(event, this);
ラムダ関数を使うと、それらの変数をキャプチャすることができます。ラムダ関数の型としてautoが使えるのも便利ですね。
MyClass::func(const EventInfo &event) {
auto doIt = [&] {
this->foo(event);
...
};
doIt();
std::function
関数ラッパーです。関数を渡す必要がある場合、以前のHatoholでは、C言語の関数ポインタや関数オブジェクト、あるいは、仮想関数が実装されたオブジェクトなどを使用していましたが、不満もありました。
C言語の関数ポインタは、クラスのメンバ関数を渡すことができません。また、非メンバ関数を渡す場合でも、その関数が使用するデータの型が定まっていない場合、次のようにvoid型のポインタにキャストして渡さざるを得ません。強い型を持つC++を使っているにもかかわらず、少しのミスでバグを誘発する可能性があります。
void registerCallback(int (*func)(void), void *data) {
...
}
int func(void *data) {
EventInfo *evt = static_cast<EventInfo *>(data);
...
}
void MyClass::foo(void) {
EventInfo *evt = getEvent();
registerCallback(func, static_cast<void *>(evt));
...
}
関数オブジェクトや仮想関数を使う場合は、そのオブジェクト内にデータを持たせることができるため、上記のような危険なキャストを削除できます。しかし、クラスや構造体を定義しなければならないので記述が煩雑になりがちでした。
void registerCallback(Func *func) {
...
}
void MyClass::foo(void) {
struct MyFunc : public Func {
EventInfo *evt;
int operator()(void) {
...
}
};
auto *func = new MyFunc();
func.evt = getEvnt();
registerCallback(func);
....
}
そこで最近は、以下のように、渡す関数をラムダ関数として定義して、呼び出し先ではstd::functionで受ける方法をしばしば使用します。かなりシンプルに記述できることがわかります。
void registerCallback(function<int (void)> func) {
...
}
void MyClass::foo(void) {
registerCallback([&] {
auto *evt = getEvent();
...
});
}
まとめ
最近のHatoholサーバではC++11の機能を使い始めています。よく使用する機能を紹介しました。この記事を読んで、新しい発見があれば、あなたのプログラムにも活用してみてください。