C++14 のラムダ式 完全解説 前編

C++14 の Committee Draft が公開された


C++14 は基本的には C++11 のマイナーバージョンアップであるが,バグフィックスのみを行っている訳ではなく,
C++11 の時点で微妙に使いにくかった機能,特にラムダ式については,大きな機能追加が行われている.


そこで,本 blog では,このエントリから数回に分けて, C++14 のラムダ式について説明してみることにする.
拙い文章になるかとは思うが,読者の理解の助けになれば幸いである.


なお,これらの記事を書くにあたって,読者に対して C++11 のラムダ式に対する知識を要求しないように心がけたが,
もしかしたら,説明不十分であり,分かりにくい部分があるかもしれない.
そのような場合には, 本の虫: lambda 完全解説 等, C++11 のラムダについて書かれた記事は多いので,
それらの記事を読んでみることを お勧めしたい.


また,以降のエントリは N3690 に基づいて書かれたものであり,今後の展開次第では大きな変更が来る可能性があること,
また筆者の規格の解釈が間違っている可能性も十分にあることは,予め言及させていただく.
もしツッコミどころがありましたら,気軽にコメントなり Twitter なりで お教え下さい.


目次:

  1. ラムダ式の基礎 (この記事で解説)
  2. ラムダ式型推論(この記事で解説)
  3. 変数のキャプチャ(中編で解説)
  4. ラムダ式の活用法(後編で解説予定)
  5. マニア向けの補足(後編で解説予定)

ラムダ式の基礎

ラムダ式とは,文字通り「式」である.


式ということは,つまり値を持つ.*1
値のない式も C++ には例外的に存在するが,ラムダ式はそのような例外ではない.
きちんと値をもった,立派な式である.
では,ラムダ式の値とは, 一体 何なのか.


ラムダ式の値は関数オブジェクトである. 名前は特にない.
C++ における関数オブジェクトとは,

int main() {
  struct plus_one_t {
    auto operator()(int x) const -> int {
      return x + 1;
    }
  };
  plus_one_t plus_one = {};
  std::cout << plus_one(0) << std::endl; // 1
}

のように, operator() が定義された,まるで関数のように使えるオブジェクトのことだった.


同じようなコードをラムダ式を使って書くと,

int main() {
  auto plus_one = [] (int x) -> int {
    return x + 1;
  };
  std::cout << plus_one(0) << std::endl; // 1
}

となる.


先ほどのコードと比較すると,クラスを予め宣言しておく必要がない,
地味だが const 指定を明示的に書かなくていい,といった点で,
かなりコードがシンプルになっていることが分かるだろう.


ラムダ式は,(この後に説明するキャプチャを使わない場合には,)要するにこれだけの機能だ.
とはいっても,慣れない人には少々奇妙に思えるかもしれないので,もう少し練習してみよう.

// 複数引数も普通に使える
auto add = [] (int x, int y) -> int { return x + y; };
add(1, 2); // 3
// 無引数でも問題ない
auto dice = [] () -> int { return std::rand() % 6 + 1; };  // 簡便のため std::rand を使う(本当は良くない)
dice();  // 1 〜 6 のどれか
// void を返す関数
auto print = [] (int x) -> void { std::cout << x << std::endl; };
print(1);  // 1 を出力

こんな感じである. まだ奇妙に感じるかもしれないが,慣れれば特に違和感は感じなくなる(と思う).
ラムダ式というものがどういうものか,これで少し分かったのではないだろうか.

-- ちょいと脱線 --
なお,上の例では,ラムダ式の結果を変数に代入して使っていたが,
ラムダ式は普通の式であるので,もちろん変数に代入しなくても使うことが出来る.
最もポピュラーなのは,関数の引数として使うことだろう.

// <algorithm> の関数と組み合わせる
std::vector<int> v = {1, 2, 3, 4, 5};
std::for_each( v.begin(), v.end(),
  [] (int x) -> void { std::cout << x << std::endl; }
);

勿論,それ以外にも使用方法は沢山ある.
そのうちの幾つかは,この記事でも追って解説するので,楽しみにしておいてほしい.
-- 脱線ここまで --

ラムダ式型推論

さて,先ほどのラムダ式では,ラムダ式の戻り値の型を明示的に書いていたが,
実のところ,ラムダ式の戻り値の型は,明示的に書かなくても,コンパイラによって推論される:

auto plus_one = [] (int x) {
  return x + 1;  // ここから戻り値の型は int と分かるので -> int は書かなくていい
};
auto print = [] (int x) {
  std::cout << x << std::endl;  // return 文が無い場合は void と推論される
};

推論に使われるのは return 文であり,たとえばラムダ式内部に return 0; と書かれていた場合には,
0 の型は int であるため,そのラムダ式の戻り値の型は int と推論される.((なお, return {1, 2, 3}; という形で書かれていた場合は,型推論は失敗する.))
ラムダ式内部に 何らかの値を返す return 文が存在しない場合には,ラムダ式の戻り値の型は void と推論される.


ラムダ式が引数を取らず,その戻り値型が自動で推論される場合,
つまりラムダ式[] () { 〜; } のような形で表される場合には,
[]{ の間に現れる () は,省略することができる.

// 戻り値がある例
auto f = [] { return 42; };

// 戻り値が void の例
auto g = [] {
  for (int i = 0; i < 10; ++i ) {
    std::cout << i << std::endl;
  }
};

上記の fg は,いずれも引数を持たない関数となる.


さて,C++11 時点のラムダ式では,戻り値が void の場合を除き,
ラムダ式本体が { return 式; } という形になっていない限り,型推論は不可能であった.


それは不便だ,ということで, C++14 のラムダ式では,その制限は撤廃される:

auto f = [] (double x) {
  if (x < 0) {
    return std::numeric_limits<double>::quiet_NaN();
  }
  else {
    return std::sqrt(x);
  }
};

上記のような式は C++11 ではコンパイルできないが, C++14 ではコンパイルできるようになる.

-- ちょいと脱線 --


なお,複数の return がある場合には,
それによって帰される値の型が一致してない場合には,コンパイルエラーとなる:

auto f = [] (double x) {
  if (x < 0) {
    return 0;
  }
  else {
    return std::sqrt(x);
  }
};

上記のコードは, 0int 型である一方, std::sqrt(x)double 型なので,
型推論に失敗してコンパイルエラーとなる.


また,戻り値の型推論によって決定される型は, C++11 でも C++14 でも,参照や const の付かない型になる.

auto deref = [] (int* p) { return *p; };  // 戻り値の型は int であって int& ではない

型推論を使いつつ参照を返したい場合には, C++14 ならば,戻り値の型に auto& を指定すればいい.

auto deref = [] (int* p) -> auto& { return *p; }; // 戻り値の型は int& になる

この他, auto const&auto&& といったものも使える.
これらの型推論のルールは,例えば auto const& ならば

auto const& result = 式;

と書いた時に decltype(result) で得られる型に等しくなる.


-- 脱線ここまで --


それに加え, C++14 では, auto を使うことで,引数に対しても型推論を行えるようになる.

auto plus_one = [] (auto x) {
  return x + 1;
};
plus_one(0);       // ok, int 型を返す
plus_one(0.0);    // ok, double 型を返す
int a[10];
plus_one(&a[0]); // ok, int* 型を返す

これは, operator() がテンプレートとして定義されたクラスを使って

struct plus_one_t {
  template<class T>
  auto operator()(T x) const {
    return x + 1; // C++14 ではラムダ以外の関数でも型推論ができる
  }
};
plus_one_t plus_one = {};

と書いたのと ほぼ同じだ(ただし,関数内部でテンプレートは書けないため,上記のコードは書かれた場所によってはコンパイルエラーになる).


この場合,引数は値渡しになるので,それでは効率が悪いと感じる場合*2には,

auto get_size = [] (auto const& x) {
  return x.size();
};

のように auto const& を用いる. この場合は

struct get_size_t {
  template<class T>
  auto operator()(T const& x) const {
    return x.size();
  }
};
get_size_t get_size = {};

と書いたのと同じだ.
同様に auto&auto&&auto const* なども使える.

-- ちょいと脱線 --
なお, x の型を調べたい場合は, decltype を使えばいい.
特に auto&& を使った場合,これは T&& による型推論と同じなので,
正しく転送するには Perfect Forward を行う必要があるが,これは decltype を使って

auto f = [] (auto&& x) { return g( std::forward<decltype(x)>(x) ); };

と書く必要がある. 複雑なことをしようとした場合には よく出るパターンなので覚えよう.
-- 脱線ここまで --


また,複数の引数を auto で渡すことも出来る.

auto add = [] (auto x, auto y) { return x + y; };

この場合,引数の型に現れた auto 1つにつき1つのテンプレート引数が導入される.

struct add_t {
  template<class T, class U>
  auto operator()(T x, U y) const {
    return x + y;
  }
};
add_t add = {};

この動作は,うっかり忘れることもあるので気をつけよう(もっとも,忘れても特に困らないが).


更に, ... を使うことで,可変個引数にも対応できる.
これは,ほぼ専ら auto&&... という形で Perfect Forward に用いると思って良い.

auto f_ = [] (auto&&... args) { return f( std::forward<decltype(args)>(args)... ); };

この機能は,多重定義された関数をテンプレートに渡したい場合に重宝する.*3

// 上記のように f_ が定義されてるとする

std::vector<int> v = {1, 2, 3, 4, 5};
std::for_each( v.begin(), v.end(), f  ); // f が多重定義されてる場合,コンパイルできない
std::for_each( v.begin(), v.end(), f_ ); // ラムダ式は関数オブジェクトなので問題ない

もちろん,それ以外の場合(例えば先頭に引数を追加したい場合)にも使える.


このように, C++14 のラムダ式は,豊富な型推論機能を持っている.
とはいえ,実のところ, C++14 では,ラムダ式ではない普通の関数も,ほぼ同等の型推論機能を持っていたりする.
が,引数に対する auto 指定はラムダ式でないと書けないし,
ならばテンプレートを使えばいいかというと,関数の内部で定義されたクラスに対しては テンプレートは使えない,
仮に関数内部でのテンプレートが解禁されても,多重定義された関数はそのままでは他の関数の引数として使えない,
といった事情があり,ラムダ式型推論機能は C++14 に不可欠なものとなっているのである.


しかし,実のところ,ラムダ式の凄さは 型推論だけに留まるものではない.
次回の記事では,ラムダ式ラムダ式たらしめている機能,「変数のキャプチャ」について説明したい.

* * * * *


今回のまとめ:

  • ラムダ式とは関数である. ローカル変数に入れたり,標準アルゴリズムの引数として使ったりできる
  • ラムダ式は関数オブジェクトを生成する. ラムダ式の本体は生成されたクラスの operator() になる
  • ラムダ式の戻り値の型を省略することで,コンパイラに推論させることができる
  • ラムダ式の引数の型を auto にすることで,引数の型も推論させられる(多相ラムダ)
  • 多相ラムダは,実装的にはテンプレートになる. テンプレートなので ... も使える
  • ラムダ式を使うことで,多重定義された関数を関数オブジェクトに変換し,関数の引数に渡すことが可能になる

*1:厳密には value category も持つ. 今回は特に そこには言及しないが,参考までに,ラムダ式value category は prvalue である. わからない人はスルーしていいよ.

*2:実際には値渡しのほうが効率が良くなるケースも多いが,流石に std::vector などの場合には値渡しは厳しい

*3:本来は 多重定義された関数も そのままテンプレートで扱えるようにするべきだが,うまく規格を定めるのは難しいだろうし,また今回の記事の本題とはズレるので,その話題については割愛させていただく.