野良C++erの雑記帳 このページをアンテナに追加 RSSフィード Twitter

2013-05-16 C++14 に追加(ry ライブラリ編の公開予定は未定です

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 と推論される.*2

ラムダ式内部に 何らかの値を返す 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 = {};

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


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

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)... ); };

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

// 上記のように 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 に不可欠なものとなっているのである.


しかし,実のところ,ラムダ式の凄さは 型推論だけに留まるものではない.

次回の記事では,ラムダ式ラムダ式たらしめている機能,「変数のキャプチャ」について説明したい.


* * * * *


今回のまとめ:

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

*2:なお, return {1, 2, 3}; という形で書かれていた場合は,型推論は失敗する.

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

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

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

リンク元