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 %s\n", &str, 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), 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 %s\n", &str, str.c_str());
 string strs = std::move(str);
 printf("String address %p in funcd B, str %s, strs %s\n", &str, str.c_str(), strs.c_str());
}

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

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

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

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

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

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

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

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

 funcd(std::move(stra));
 printf("String address %p in main F, stra %s\n", &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<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 %s\n", &str, str.c_str());
 string strs = std::move(str);
 printf("String address %p in bar B, str %s, strs %s\n", &str, str.c_str(), strs.c_str());
}

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

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

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

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

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

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

 auto funcg = foo(std::move(str));
 printf("String address %p in main B, str %s\n", &str, 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,str, strstest
Stringaddress0x7ffc9fe7c5c0 (foo lambda) E, strtest
Stringaddress0x7ffc9fe7c5c0 in barA, strtest
Stringaddress0x7ffc9fe7c5c0 in barB,str, 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 捕获模式与右值引用内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++标准之(ravalue reference) 右值引用介绍

    1.右值引用引入的背景 临时对象的产生和拷贝所带来的效率折损,一直是C++所为人诟病的问题.但是C++标准允许编译器对于临时对象的产生具有完全的自由度,从而发展出了CopyElision.RVO(包括NRVO)等编译器优化技术,它们可以防止某些情况下临时对象产生和拷贝.下面简单地介绍一下CopyElision.RVO,对此不感兴趣的可以直接跳过: (1)CopyElision CopyElision技术是为了防止某些不必要的临时对象产生和拷贝,例如: 复制代码 代码如下: structA{ A(

  • 详解C++11中的右值引用与移动语义

    C++11的一个最主要的特性就是可以移动而非拷贝对象的能力.很多情况都会发生对象的拷贝,有时对象拷贝后就立即销毁,在这些情况下,移动而非拷贝对象会大幅度提升性能. 右值与右值引用 为了支持移动操作,新标准引入了一种新的引用类型--右值引用,就是必须绑定到右值的引用.我们通过&&而不是&来获得右值引用.右值引用一个重要的特性就是只能绑定到将要销毁的对象. 左值和右值是表达式的属性,一些表达式生成或要求左值,而另一些则生成或要求右值.一般而言,一个左值表达式表示的是一个对象的身份,而右

  • 浅析C++11中的右值引用、转移语义和完美转发

    1. 左值与右值: C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:可以取地址的,有名字的,非临时的就是左值;不能取地址的,没有名字的,临时的就是右值. 可见立即数,函数返回的值等都是右值;而非匿名对象(包括变量),函数返回的引用,const对象等都是左值. 从本质上理解,创建和销毁由编译器幕后控制的,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象),例如: int& fo

  • C++11右值引用和std::move语句实例解析(推荐)

    右值引用(及其支持的Move语意和完美转发)是C++0x将要加入的最重大语言特性之一.从实践角度讲,它能够完美解决C++中长久以来为人所诟病的临时对象效率问题.从语言本身讲,它健全了C++中的引用类型在左值右值方面的缺陷.从库设计者的角度讲,它给库设计者又带来了一把利器.从库使用者的角度讲,不动一兵一卒便可以获得"免费的"效率提升- 下面用实例来深入探讨右值引用. 1.什么是左值,什么是右值,简单说左值可以赋值,右值不可以赋值.以下面代码为例,"A a = getA();&q

  • C++11的右值引用的具体使用

    C++11 引入了 std::move 语义.右值引用.移动构造和完美转发这些特性.由于这部分篇幅比较长,分为3篇来进行阐述. 在了解这些特性之前,我们先来引入一些问题. 一.问题导入 函数返回值是传值的时候发生几次对象构造.几次拷贝? 函数的形参是值传递的时候发生几次对象构造? 让我们先来看一段代码, // main.cpp #include <iostream> using namespace std; class A{ public: A(){ cout<<"cla

  • 浅谈C++左值引用和右值引用

    实例如下: #include<iostream> #include<utility> #include<vector> using namespace std; int f(); int main() { vector<int>vi(100); int i=42; int &&r1=i;//error不能把右值引用绑到左值上 int &&r2=10; int &r3=i; int &r4=10;//error非

  • C++11右值引用和转发型引用教程详解

    右值引用 为了解决移动语义及完美转发问题,C++11标准引入了右值引用(rvalue reference)这一重要的新概念.右值引用采用T&&这一语法形式,比传统的引用T&(如今被称作左值引用 lvalue reference)多一个&. 如果把经由T&&这一语法形式所产生的引用类型都叫做右值引用,那么这种广义的右值引用又可分为以下三种类型: 无名右值引用 具名右值引用 转发型引用 无名右值引用和具名右值引用的引入主要是为了解决移动语义问题. 转发型引用的引

  • 深入解读C++中的右值引用

    右值引用(及其支持的Move语意和完美转发)是C++0x将要加入的最重大语言特性之一,这点从该特性的提案在C++ - State of the Evolution列表上高居榜首也可以看得出来. 从实践角度讲,它能够完美解决C++中长久以来为人所诟病的临时对象效率问题.从语言本身讲,它健全了C++中的引用类型在左值右值方面的缺陷.从库设计者的角度讲,它给库设计者又带来了一把利器.从库使用者的角度讲,不动一兵一卒便可以获得"免费的"效率提升- 在标准C++语言中,临时量(术语为右值,因其出

  • 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", &a

  • C++右值引用与移动构造函数基础与应用详解

    目录 1.右值引用 1.1左值右值的纯右值将亡值右值 1.2右值引用和左值引用 2.移动构造函数 2.1完美的移动转发 1.右值引用 右值引用是 C++11 引入的与 Lambda 表达式齐名的重要特性之一.它的引入解决了 C++ 中大量的历史遗留问题, 消除了诸如 std::vector.std::string 之类的额外开销, 也才使得函数对象容器 std::function 成为了可能. 1.1左值右值的纯右值将亡值右值 要弄明白右值引用到底是怎么一回事,必须要对左值和右值做一个明确的理解

  • 深入学习C++智能指针之shared_ptr与右值引用的方法

    目录 1. 介绍 2. 初始化方法 2.1 通过构造函数初始化 2.2 通过拷贝和移动构造函数初始化 2.3 通过 std::make_shared 初始化 2.4 通过 reset 方法初始化 3. 获取原始指针 4. 指定删除器 5. 参考链接 1. 介绍 在 C++ 中没有垃圾回收机制,必须自己释放分配的内存,否则就会造成内存泄露.解决这个问题最有效的方法是使用智能指针(smart pointer).智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用

  • 一文搞懂C++11万能引用和右值引用

    目录 前言 正文 万能引用 结语 参考: 前言 我们通过一个问题来进入今天的话题:1.形如 “type&&” 的结构,就是右值引用吗?2.以下哪些属于右值引用? ① void fun(Widget && param);② Widget && var1= Widget();③ auto && var2 = var1;④ template<typename T> void f(std::vector<T>&&

  • C++右值引用与move和forward函数的使用详解

    目录 1.右值 1.1 简介 1.2 右值引用 1.3 右值引用的意义 2.move 3.foward 1.右值 1.1 简介 首先区分一下左右值: 左值是指存储在内存中.有明确存储地址(可取地址)的数据: 右值是指可以提供数据值的数据(不可取地址) 如int a=123:123是右值, a是左值.总的来说 可以对表达式取地址(&)就是左值,否则为右值 而C++11 中右值又可以分为两种: 纯右值:非引用返回的临时变量.运算表达式产生的临时变量如a+b.原始字面量和 lambda 表达式等 将亡

随机推荐