ブログ・ア・ラ・クレーム

技術的なメモとかライフログとか。

C++ のラムダ式のデフォルトキャプチャの対象となる変数について確認していた

C++初学者なので勉強のため会社の有志の人々と Effective Modern C++ の輪読会をやっているのですが、その際にラムダ式のデフォルトキャプチャの挙動に関する話題になり、個人的に無性に気になってしまったので調べてみました。 もし誤りや補足などございましたら、コメント等でご指摘いただければ幸いです。

ながすぎる; よんでない

疑問

デフォルトコピー・参照キャプチャ([=], [&])を記述した際のラムダ式のデータメンバとしては、ラムダ式を定義した外のスコープの変数すべてが対象になるのか、必要なメンバしか持たないのか、という点が気になりました。 例えば外のスコープで下記のようにクソデカイ容量の変数が存在したとして、必要であろうがなかろうが問答無用でコピーされうるのか・・・。

using namespace std;

int main() {
  vector<int> large_vec;

  ... // 紆余曲折あって large_vec のキャパシティがクソデカくなっていると想定する

  auto lambda = [=]{}; // lambda のメンバにはクソデカイ vector は、例え使ってなくても含まれるのか??

  return 0;
}

輪読会参加メンバーで打ち立てた予測としては、必要な変数のみコピーされるというものでした

回答

言語仕様上、必要な変数のみコピーされるようです。 (・・・但し後述の通り、実際にデバッガ当てて調べていった感じ、そうではない箇所もあり・・・)

調査結果

言語仕様を読んでみた

今回は The Standard : Standard C++ に掲載されている C++14 の仕様のドラフト N4296 を眺めてみました。

ラムダ式に関する記述がある 5.1.2 節の 12,13 番 に、今回扱う疑問点に関するっぽい記述を発見しました。曰く

デフォルトキャプチャを持つラムダ式は ... (中略) ... compound-statement(ラムダ式の {} の中の文のこと) が下記の条件である場合エンティティ(this と変数のこと) を暗黙的にキャプチャする。 エンティティが odr-used である、もしくは ... (略)

... (略) ラムダ式によってキャプチャされるエンティティは、ラムダ式を含むスコープで ord-used である ... (略)

odr-used な変数がキャプチャの対象になりそうです。 ・・・で、 odr-used というのは何だよという話になりますが、大雑把にいうと定義を必要とする変数の使用のことのようです。 つまるところ、上記ドラフトの記述から「定義を必要としない変数の使用しかされていない(not odr-used な)変数はデフォルトキャプチャを書いてもキャプチャされない」と言えそうです。

実際のコンパイル結果

実際どのようにキャプチャされるのか確認してみました。今回は下記のソースコードを手元の Mac OS X 10.11 にて下記の環境でコンパイルし、 lldb でラムダ式のキャプチャ結果を拾ってみます。

$ g++ --version
Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.11.sdk/usr/include/c++/4.2.1
Apple LLVM version 7.3.0 (clang-703.0.31)
Target: x86_64-apple-darwin15.6.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin

デフォルトコピーキャプチャ以外の振る舞いもついでに見てみたいので、幾つかのキャプチャとラムダ式内の文のパターンを用意してみました。

using namespace std;

int main() {
  vector<int> unused_data = {0};
  vector<int> data = {0};

  auto cpcap_ex = [data]{};
  auto cpcap = [=]{ cout << data.size() << endl; };
  auto cpcap_emptyimpl = [=]{};

  auto refcap = [&]{ cout << data.size() << endl; };
  auto refcap_ex = [&data]{};
  auto refcap_emptyimpl = [&]{};

  return 0;
}

デバッガで動作確認したい都合、今回は -g を付けてコンパイルします。

$ g++ -g -std=c++0x test.cpp

コンパイル後に lldb で return あたりの行でブレークポイントを貼って各変数の内容を見てみます。

$ lldb a.out
...
(lldb) b test.cpp:<行数>
...
(lldb) r
...
(lldb) p cpcap_ex
((anonymous class)) $0 = {
  data = size=1 {
    [0] = 0
  }
}
(lldb) p cpcap
((anonymous class)) $1 = {
  data = size=1 {
    [0] = 0
  }
}
(lldb) p cpcap_emptyimpl
((anonymous class)) $2 = {}
(lldb) p refcap
((anonymous class)) $3 = {
  data = size=1
}
(lldb) p refcap_ex
((anonymous class)) $4 = {
  data = size=1
}
(lldb) p refcap_emptyimpl
((anonymous class)) $5 = {}

表題とは外れてしまいますが、まずデフォルトキャプチャではなく明示的にキャプチャした場合は、 odr-used とか関係なくキャプチャされているようですね。 また、デフォルトキャプチャかつラムダ式の文内で data.size() みたいな感じで odr-used すると、対象の変数はキャプチャされているようです。 更にデフォルトキャプチャでラムダ式の文を空にした場合は、何もキャプチャされないという結果になりました。

ちなみに下記のようにラムダ式の文内に odr-used な式を書いてみたのですが・・・

class undef *up;
auto cpcap_undef = [=]{ up; }; // {} 内の式は discarded-value expression であり、 up は odr-used

この場合はキャプチャされているっぽく見えます。自分の仕様の解釈が誤っているのか、あるいは何か読み落としているのか・・・教えてえろいひと!

(lldb) p cpcap_undef
((anonymous class)) $0 = (up = 0x0000000000000000)