C++lambdaキャプチャモードと右値参照
14508 ワード
Lambda式と右値参照はC++11の2つの非常に有用な特性である.
Lambda式は実際にはコンパイラによって作成される
このコードの出力は次のとおりです.
上で呼び出された
呼び出し
呼び出し
移動構造後、
ここでは移動割付後の
また、呼び出し
この行のコードはコンパイルに失敗します.コンパイルエラー情報は次のとおりです.
Lambdaで値でキャプチャされた右値オブジェクトは、lambdaのstd::functionオブジェクトにキャプチャされた右値オブジェクトのコピーが作成されただけで、元の右値は変更されません.
次に、サンプルコードを見てみましょう.
上記のコードは、実行時に次のように出力されます.
これは、
ただし、
サンプルコードを見てみましょう.
上記のコードの出力は次のとおりです.
関数
前述したように、lambda式では、右値参照を値で取得すると、コンパイラがそのlambda式のために生成した
関数
関数
左の値を右の値参照にバインドできません.
関数
関数
関数
Done.
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);
移動構造後、
funca
とfuncd
を呼び出したときの出力は、移動構造時にメンバー毎の移動構造をしていたことがわかる.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
ただし、
funcb
const左値参照をパラメータとして受信した場合、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()
関数で定義されているスタック上の一時オブジェクトstra
foo()
関数呼び出しが終了するとスタック上の一時オブジェクトが解放されるmain()
関数で呼び出されているfuncg
実際に無効なオブジェクトにアクセスしているため問題が発生する.Done.