現在位置: ホーム / みらくるブログ / HatoholにおけるC++11の活用

HatoholにおけるC++11の活用

Hatoholサーバは、C++で記述されています。最近の開発ではC++11で新たに取り入れられた機能も活用しています。本稿では、どのような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);
}
forの第一パラメータに&(参照)を使用しています。参照にしない場合、コピーされた変数(オブジェクト)が渡されます。当該オブジェクトを読むだけの用途では、参照にするほうが当然パフォーマンスに優れます。その際、オブジェクトの誤った変更を防止するため、constも同時に使用しています。

型推論

型を自動的に決定する機能で、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();
...
});
}
上記は、簡単に説明するためのサンプルなので、その上の2つの例とは厳密には振る舞いが一致していません。getEvent()を呼ぶタイミングが、本節の最初の2つの例では、registerCallback()を呼ぶ直前なのに対して、最後のラムダ関数を使ったものでは、呼びだされた後になっています。

まとめ

最近のHatoholサーバではC++11の機能を使い始めています。よく使用する機能を紹介しました。この記事を読んで、新しい発見があれば、あなたのプログラムにも活用してみてください。