1. 程式人生 > 程式設計 >C++ lambda 捕獲模式與右值引用的使用

C++ lambda 捕獲模式與右值引用的使用

lambda 表示式和右值引用是 C++11 的兩個非常有用的特性。

lambda 表示式實際上會由編譯器建立一個std::function 物件,以值的方式捕獲的變數則會由編譯器複製一份,在std::function 物件中建立一個對應的型別相同的 const 成員變數,如下面的這段程式碼:

int main(){
 std::string str = "test";
 printf("String address %p in main,str %s\n",&str,str.c_str());
 auto funca = [str]() {
 printf("String address %p (main lambda),str.c_str());
 };

 std::function<void()> funcb = funca;
 std::function<void()> funcc;
 funcc = funca;

 printf("funca\n");
 funca();

 std::function<void()> funcd = std::move(funca);
 printf("funca\n");
 funca();

 printf("funcb\n");
 funcb();

 std::function<void()> funce;
 funce = std::move(funcb);

 printf("funcb\n");
// funcb();

 printf("funcc\n");
 funcc();

 printf("funcd\n");
 funcd();

 printf("funce\n");
 funce();

// std::function<void(int)> funcf = funce;

 return 0;
}

這段程式碼的輸出如下:

Stringaddress0x7ffd9aaab720 in main,strtest
funca
Stringaddress0x7ffd9aaab740 (main lambda),str
funcb
Stringaddress0x55bdd2160280 (main lambda),strtest
funcb
funcc
Stringaddress0x55bdd21602b0 (main lambda),strtest
funcd
Stringaddress0x55bdd21602e0 (main lambda),strtest
funce
Stringaddress0x55bdd2160280 (main lambda),strtest

由上面呼叫funca 時的輸出,可以看到 lambda 表示式以值的方式捕獲的物件 str,其地址在 lambda 表示式內部和外部是不同的。

std::function 類物件和普通的魔板類物件一樣,可以拷貝構造,如:

std::function<void()> funcb = funca;

由呼叫funcb 時的輸出,可以看到拷貝構造時是做了逐成員的拷貝構造。

std::function 類物件可以賦值,如:

std::function<void()> funcc;
funcc = funca;

由呼叫funcc 時的輸出,可以看到賦值時是做了逐成員的賦值。

std::function 類物件可以移動構造,如:

std::function<void()> funcd = std::move(funca);

由移動構造之後,呼叫funca 和funcd 時的輸出,可以看到移動構造時是做了逐成員的移動構造。

std::function 類物件可以移動賦值,如:

 std::function<void()> funce;
 funce = std::move(funcb);

 printf("funcb\n");
// funcb();

這裡把移動賦值之後對funcb 的呼叫註釋掉了,這是因為,作為源的funcb 在移動賦值之後被呼叫是,會丟擲異常,如:

String address 0x562334c34280 (main lambda),str test
funcb
terminate called after throwing aninstanceof 'std::bad_function_call'
 what(): bad_function_call

同時,由呼叫funce 時的輸出可以看到,該輸出與funcb 在移動賦值之前被呼叫時的輸出完全相同。即移動賦值是將物件整體 move 走了,這與移動構造時的行為不太一樣。

std::function 類物件的拷貝構造或者賦值,也需要滿足型別匹配原則,如:

std::function<void(int)> funcf = funce;

這行程式碼會造成編譯失敗,編譯錯誤資訊如下:

../src/DemoTest.cpp: In function ‘intmain()':
../src/DemoTest.cpp:64:36: error: conversion from ‘std::function<void()>' to non-scalar type ‘std::function<void(int)>' requested
std::function<void(int)> funcf = funce;
^~~~~
make: *** [src/DemoTest.o] Error 1
src/subdir.mk:18: recipe for target 'src/DemoTest.o' failed

在 lambda 中以值的方式捕獲的右值物件,只是在 lambda 的 std::function 物件中做了一份被捕獲的右值物件的拷貝,而原來的右值則沒有任何改變。

接下來再來看一段示例程式碼:

#include<iostream>
#include<functional>
#include<string>

using namespace std;

void funcd(std::string&&str){
 printf("String address %p in funcd A,str.c_str());
 string strs = std::move(str);
 printf("String address %p in funcd B,str %s,strs %s\n",str.c_str(),strs.c_str());
}

void funcc(std::stringstr){
 printf("String address %p in funcc,str.c_str());
}

void funcb(std::string&str){
 printf("String address %p in funcb,str.c_str());
}

void funca(std::string&&str){
 printf("String address %p in funca A,str.c_str());
 std::string stra = str;
 printf("String address %p in funca B,stra %s\n",stra.c_str());
}

int main(){
 std::string str = "test";
 printf("String address %p in main A,str.c_str());

 funca(std::move(str));
 printf("String address %p in main B,str.c_str());

// funcb(std::move(str));
 printf("String address %p in main C,str.c_str());

 funcc(std::move(str));
 printf("String address %p in main D,str.c_str());

 std::string stra = "testa";
 printf("String address %p in main E,&stra,stra.c_str());

 funcd(std::move(stra));
 printf("String address %p in main F,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<char>&}' to an rvalue of type ‘std::remove_reference<std::__cxx11::basic_string<char>&>::type {aka std::__cxx11::basic_string<char>}'
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<iostream>
#include<functional>
#include<string>

using namespace std;

void bar(std::string&&str){
 printf("String address %p in bar A,str.c_str());
 string strs = std::move(str);
 printf("String address %p in bar B,strs.c_str());
}

std::function<void()> bar_bar(std::string &&str) {
 auto funf = [&str]() {
 printf("String address %p (foo lambda) F,str.c_str());
 };
 return funf;
}

std::function<void()> foo(std::string &&str) {
 printf("String address %p in foo A,str.c_str());

// auto funa = [str]() {
// printf("String address %p (foo lambda) A,str.c_str());
// bar(str);
// };
// funa();
//
// auto funb = [str]() {
// printf("String address %p (foo lambda) B,str.c_str());
// bar(std::move(str));
// };
// funb();

// auto func = [str]() mutable {
// printf("String address %p (foo lambda) C,str.c_str());
// bar(str);
// };
// func();

 auto fund = [str]() mutable {
 printf("String address %p (foo lambda) D,str.c_str());
 bar(std::move(str));
 };
 fund();

 auto fune = [&str]() {
 printf("String address %p (foo lambda) E,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.c_str());

 auto funcg = foo(std::move(str));
 printf("String address %p in main B,str.c_str());

 funcg();

 return 0;
}

上面這段程式碼的輸出如下:

Stringaddress0x7ffc9fe7c5c0 in main A,strtest
Stringaddress0x7ffc9fe7c5c0 in foo A,strtest
Stringaddress0x7ffc9fe7c540 (foo lambda) D,strtest
Stringaddress0x7ffc9fe7c540 in barA,strtest
Stringaddress0x7ffc9fe7c540 in barB,strstest
Stringaddress0x7ffc9fe7c5c0 (foo lambda) E,strtest
Stringaddress0x7ffc9fe7c5c0 in barA,strtest
Stringaddress0x7ffc9fe7c5c0 in barB,strstest
Stringaddress0x7ffc9fe7c5c0 in main B,str
Stringaddress0x7ffc9fe7c560 (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<char>&&}' to lvalue of type ‘const string {aka const std::__cxx11::basic_string<char>}'
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<char>&&}' to ‘std::remove_reference<const std::__cxx11::basic_string<char>&>::type {aka const std::__cxx11::basic_string<char>}' 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<char>&&}' to lvalue of type ‘std::__cxx11::string {aka std::__cxx11::basic_string<char>}'
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 實際在訪問一個無效的物件,因而出現問題。

到此這篇關於C++ lambda 捕獲模式與右值引用的使用的文章就介紹到這了,更多相關C++ lambda 捕獲模式與右值引用內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!