C++lambdaキャプチャモードと右値参照


Lambda式と右値参照はC++11の2つの非常に有用な特性である.
Lambda式は実際にはコンパイラによって作成されるstd::functionオブジェクト、値で取得される変数はコンパイラによってコピーされ、std::functionオブジェクトには次のコードのような対応するタイプの同じconstメンバー変数が作成されます.
int main() {
  std::string str = "test";
  printf("String address %p in main, str %s
", &str, str.c_str()); auto funca = [str]() { printf("String address %p (main lambda), str %s
", &str, str.c_str()); }; std::function funcb = funca; std::function funcc; funcc = funca; printf("funca
"); funca(); std::function funcd = std::move(funca); printf("funca
"); funca(); printf("funcb
"); funcb(); std::function funce; funce = std::move(funcb); printf("funcb
"); // funcb(); printf("funcc
"); funcc(); printf("funcd
"); funcd(); printf("funce
"); funce(); // std::function funcf = funce; return 0; }

このコードの出力は次のとおりです.
String address 0x7ffd9aaab720 in main, str test
funca
String address 0x7ffd9aaab740 (main lambda), str test
funca
String address 0x7ffd9aaab740 (main lambda), str 
funcb
String address 0x55bdd2160280 (main lambda), str test
funcb
funcc
String address 0x55bdd21602b0 (main lambda), str test
funcd
String address 0x55bdd21602e0 (main lambda), str test
funce
String address 0x55bdd2160280 (main lambda), str test

上で呼び出されたfuncaの出力から、lambda式が値でキャプチャされたオブジェクトstrが見え、そのアドレスはlambda式の内部と外部で異なる.std::function類オブジェクトは通常の魔板類オブジェクトと同様に、構造をコピーすることができる.
  std::function funcb = funca;

呼び出しfuncb時の出力により、コピー構造時にメンバー毎のコピー構造が作られていることがわかる.std::functionクラスオブジェクトには次のような値を割り当てることができます.
  std::function funcc;
  funcc = funca;

呼び出しfunccの出力により、付与時にメンバー毎の付与が行われていることがわかる.std::functionクラスオブジェクトは構造を移動可能、例えば:
  std::function funcd = std::move(funca);

移動構造後、funcafuncdを呼び出したときの出力は、移動構造時にメンバー毎の移動構造をしていたことがわかる.std::functionクラスオブジェクトは、次のように割り当てを移動できます.
  std::function funce;
  funce = std::move(funcb);

  printf("funcb
"); // funcb();

ここでは移動割付後のfuncbの呼び出しについてコメントしたが、これは、ソースであるfuncb移動割付後に呼び出されるのは、次のような異常が投げ出されるからである.
String address 0x562334c34280 (main lambda), str test
funcb
terminate called after throwing an instance of 'std::bad_function_call'
  what():  bad_function_call

また、呼び出しfunceのときの出力は、funcb割り当てを移動する前に呼び出されたときの出力と全く同じであることがわかる.すなわち,移動付与はオブジェクト全体をmoveしたものであり,構造を移動するときの挙動とは異なる.std::functionクラスオブジェクトのコピー構造または付与も、タイプマッチングの原則を満たす必要がある.
  std::function funcf = funce;

この行のコードはコンパイルに失敗します.コンパイルエラー情報は次のとおりです.
../src/DemoTest.cpp: In function ‘int main()’:
../src/DemoTest.cpp:64:36: error: conversion from ‘std::function’ to non-scalar type ‘std::function’ requested
   std::function funcf = funce;
                                    ^~~~~
make: *** [src/DemoTest.o] Error 1
src/subdir.mk:18: recipe for target 'src/DemoTest.o' failed

Lambdaで値でキャプチャされた右値オブジェクトは、lambdaのstd::functionオブジェクトにキャプチャされた右値オブジェクトのコピーが作成されただけで、元の右値は変更されません.
次に、サンプルコードを見てみましょう.
#include 
#include 
#include 

using namespace std;

void funcd(std::string &&str) {
  printf("String address %p in funcd A, str %s
", &str, str.c_str()); string strs = std::move(str); printf("String address %p in funcd B, str %s, strs %s
", &str, str.c_str(), strs.c_str()); } void funcc(std::string str) { printf("String address %p in funcc, str %s
", &str, str.c_str()); } void funcb(std::string &str) { printf("String address %p in funcb, str %s
", &str, str.c_str()); } void funca(std::string &&str) { printf("String address %p in funca A, str %s
", &str, str.c_str()); std::string stra = str; printf("String address %p in funca B, str %s, stra %s
", &str, str.c_str(), stra.c_str()); } int main() { std::string str = "test"; printf("String address %p in main A, str %s
", &str, str.c_str()); funca(std::move(str)); printf("String address %p in main B, str %s
", &str, str.c_str()); // funcb(std::move(str)); printf("String address %p in main C, str %s
", &str, str.c_str()); funcc(std::move(str)); printf("String address %p in main D, str %s
", &str, str.c_str()); std::string stra = "testa"; printf("String address %p in main E, stra %s
", &stra, stra.c_str()); funcd(std::move(stra)); printf("String address %p in main F, stra %s
", &stra, stra.c_str()); return 0; }

上記のコードは、実行時に次のように出力されます.
String address 0x7ffc833f4660 in main A, str test
String address 0x7ffc833f4660 in funca A, str test
String address 0x7ffc833f4660 in funca B, str test, stra test
String address 0x7ffc833f4660 in main B, str test
String address 0x7ffc833f4660 in main C, str test
String address 0x7ffc833f4680 in funcc, str test
String address 0x7ffc833f4660 in main D, str 
String address 0x7ffc833f4680 in main E, stra testa
String address 0x7ffc833f4680 in funcd A, str testa
String address 0x7ffc833f4680 in funcd B, str , strs testa
String address 0x7ffc833f4680 in main F, stra 
funca関数は右値参照をパラメータとして受け取り、funca関数内部および関数呼び出し前後の出力から分かるstd::move()自身は何もせず、単に呼び出しstd::move()元のオブジェクトの内容をどこにも移動しない.std::move()単純な強制型変換であり、左値を右値参照に変換する.また、パラメータとして右の値の参照を使用してオブジェクトを構築しても、右の値の参照によって参照されるオブジェクトには影響しません.funcb関数は左値参照をパラメータとして受信し、上のコードでは、次のような行が注記されています.
//  funcb(std::move(str));

これは、funcbパラメータとして右値参照で呼び出すことができないためである.パラメータとして右参照、パラメータとして左参照を受信する関数を呼び出すfuncbコンパイルに失敗します.
g++ -O0 -g3 -Wall -c -fmessage-length=0 -MMD -MP -MF"src/DemoTest.d" -MT"src/DemoTest.o" -o "src/DemoTest.o" "../src/DemoTest.cpp"
../src/DemoTest.cpp: In function ‘int main()’:
../src/DemoTest.cpp:34:18: error: cannot bind non-const lvalue reference of type ‘std::__cxx11::string& {aka std::__cxx11::basic_string&}’ to an rvalue of type ‘std::remove_reference<:__cxx11::basic_string>&>::type {aka std::__cxx11::basic_string}’
   funcb(std::move(str));
         ~~~~~~~~~^~~~~
../src/DemoTest.cpp:17:6: note:   initializing argument 1 of ‘void funcb(std::__cxx11::string&)’
 void funcb(std::string &str) {
      ^~~~~
src/subdir.mk:18: recipe for target 'src/DemoTest.o' failed
make: *** [src/DemoTest.o] Error 1

ただし、funcbconst左値参照をパラメータとして受信した場合、void funcb(const std::string &str)のように、この関数を呼び出す際には、右値参照をパラメータとして使用することができ、このときfuncbの動作はfuncaとほぼ同じである.funcc関数は左値をパラメータとして受信し、funcc関数内部および関数呼び出し前後の出力から分かるように、左値が受信者であるため、入力された右値参照で参照されたオブジェクトの値がmoveされ、関数のパラメータスタックオブジェクトに入った.funcd関数はfunca関数と同様に右値参照をパラメータとして受信するがfuncd特別な点は、関数内部において右値が新たなオブジェクトを構築しているため、右値参照元のオブジェクトの値がmoveされ、新たな構造のオブジェクトに入ることである.
サンプルコードを見てみましょう.
#include 
#include 
#include 

using namespace std;

void bar(std::string &&str) {
  printf("String address %p in bar A, str %s
", &str, str.c_str()); string strs = std::move(str); printf("String address %p in bar B, str %s, strs %s
", &str, str.c_str(), strs.c_str()); } std::function bar_bar(std::string &&str) { auto funf = [&str]() { printf("String address %p (foo lambda) F, stra %s
", &str, str.c_str()); }; return funf; } std::function foo(std::string &&str) { printf("String address %p in foo A, str %s
", &str, str.c_str()); // auto funa = [str]() { // printf("String address %p (foo lambda) A, str %s
", &str, str.c_str()); // bar(str); // }; // funa(); // // auto funb = [str]() { // printf("String address %p (foo lambda) B, str %s
", &str, str.c_str()); // bar(std::move(str)); // }; // funb(); // auto func = [str]() mutable { // printf("String address %p (foo lambda) C, str %s
", &str, str.c_str()); // bar(str); // }; // func(); auto fund = [str]() mutable { printf("String address %p (foo lambda) D, str %s
", &str, str.c_str()); bar(std::move(str)); }; fund(); auto fune = [&str]() { printf("String address %p (foo lambda) E, str %s
", &str, str.c_str()); bar(std::move(str)); }; fune(); std::string stra = "testa"; return bar_bar(std::move(stra)); } int main() { std::string str = "test"; printf("String address %p in main A, str %s
", &str, str.c_str()); auto funcg = foo(std::move(str)); printf("String address %p in main B, str %s
", &str, str.c_str()); funcg(); return 0; }

上記のコードの出力は次のとおりです.
String address 0x7ffc9fe7c5c0 in main A, str test
String address 0x7ffc9fe7c5c0 in foo A, str test
String address 0x7ffc9fe7c540 (foo lambda) D, str test
String address 0x7ffc9fe7c540 in bar A, str test
String address 0x7ffc9fe7c540 in bar B, str , strs test
String address 0x7ffc9fe7c5c0 (foo lambda) E, str test
String address 0x7ffc9fe7c5c0 in bar A, str test
String address 0x7ffc9fe7c5c0 in bar B, str , strs test
String address 0x7ffc9fe7c5c0 in main B, str 
String address 0x7ffc9fe7c560 (foo lambda) F, stra ����

関数foo()で定義されているfunaおよび対funaの呼び出しが注釈されているのは、このコードがコンパイルに失敗するためであり、具体的なエラー情報は以下の通りである.
Invoking: GCC C++ Compiler
g++ -O0 -g3 -Wall -c -fmessage-length=0 -MMD -MP -MF"src/DemoTest.d" -MT"src/DemoTest.o" -o "src/DemoTest.o" "../src/DemoTest.cpp"
../src/DemoTest.cpp: In lambda function:
../src/DemoTest.cpp:25:12: error: cannot bind rvalue reference of type ‘std::__cxx11::string&& {aka std::__cxx11::basic_string&&}’ to lvalue of type ‘const string {aka const std::__cxx11::basic_string}’
     bar(str);
            ^
../src/DemoTest.cpp:7:6: note:   initializing argument 1 of ‘void bar(std::__cxx11::string&&)’
 void bar(std::string &&str) {
      ^~~
src/subdir.mk:18: recipe for target 'src/DemoTest.o' failed
make: *** [src/DemoTest.o] Error 1

前述したように、lambda式では、右値参照を値で取得すると、コンパイラがそのlambda式のために生成したstd::functionクラスにconstオブジェクトが生成され、constオブジェクトは右値参照として受信した右値参照をパラメータとする関数を呼び出すことができません.
関数foo()で定義されているfunbに対してfunaに対してbar()が呼び出されたときはstrに包まれたstd::move().しかし、コンパイルに失敗します.エラーメッセージは次のとおりです.
Invoking: GCC C++ Compiler
g++ -O0 -g3 -Wall -c -fmessage-length=0 -MMD -MP -MF"src/DemoTest.d" -MT"src/DemoTest.o" -o "src/DemoTest.o" "../src/DemoTest.cpp"
../src/DemoTest.cpp: In lambda function:
../src/DemoTest.cpp:31:18: error: binding reference of type ‘std::__cxx11::string&& {aka std::__cxx11::basic_string&&}’ to ‘std::remove_reference&>::type {aka const std::__cxx11::basic_string}’ discards qualifiers
     bar(std::move(str));
         ~~~~~~~~~^~~~~
../src/DemoTest.cpp:7:6: note:   initializing argument 1 of ‘void bar(std::__cxx11::string&&)’
 void bar(std::string &&str) {
      ^~~
make: *** [src/DemoTest.o] Error 1
src/subdir.mk:18: recipe for target 'src/DemoTest.o' failed
funbでは、strはconst対象なので、やはりダメです.
関数foo()で定義されているfuncに対してfunaに対してmutable修飾が加えられている.コンパイルに失敗します.エラーメッセージは次のとおりです.
Invoking: GCC C++ Compiler
g++ -O0 -g3 -Wall -c -fmessage-length=0 -MMD -MP -MF"src/DemoTest.d" -MT"src/DemoTest.o" -o "src/DemoTest.o" "../src/DemoTest.cpp"
../src/DemoTest.cpp: In lambda function:
../src/DemoTest.cpp:37:12: error: cannot bind rvalue reference of type ‘std::__cxx11::string&& {aka std::__cxx11::basic_string&&}’ to lvalue of type ‘std::__cxx11::string {aka std::__cxx11::basic_string}’
     bar(str);
            ^
../src/DemoTest.cpp:7:6: note:   initializing argument 1 of ‘void bar(std::__cxx11::string&&)’
 void bar(std::string &&str) {
      ^~~
make: *** [src/DemoTest.o] Error 1
src/subdir.mk:18: recipe for target 'src/DemoTest.o' failed

左の値を右の値参照にバインドできません.
関数foo()で定義されているfundに対してfuncに対してbar()が呼び出されたときはstrに包まれたstd::move().このときやっとコンパイルに成功し、move constのstr.
関数foo()で定義されているfuneは、funbに対して右値参照が参照として取り込まれている.funeで呼び出すbar()は、foo()直接呼び出すbar()と同じです.
関数foo()で、パラメータとして右値参照を受信した関数を呼び出すbar_bar()関数を生成する.関数bar_bar()でlambdaで定義された関数オブジェクトfunfを参照して右の値を取得し、lambdaで変更オブジェクトにアクセスします.このlambdaはbar_bar()関数として生成される関数オブジェクトである.foo()で呼び出されたbar_bar()関数スタックに定義された一時オブジェクトstraに転送され、bar_bar()返された関数オブジェクトを戻り値として返す.main()関数でfuncg受信foo()関数で返された関数オブジェクトを呼び出しfuncgを呼び出すと、crashが発生したり、文字化けしが表示されたりします.crashや文字化けしは、funfでアクセスしているstrオブジェクトが実際にfoo()関数で定義されているスタック上の一時オブジェクトstrafoo()関数呼び出しが終了するとスタック上の一時オブジェクトが解放されるmain()関数で呼び出されているfuncg実際に無効なオブジェクトにアクセスしているため問題が発生する.
Done.