标签搜索

目 录CONTENT

文章目录

C++11常用新特性(二).md

小小城
2021-10-08 / 0 评论 / 0 点赞 / 6 阅读 / 11,566 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2022-05-02,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

C++11常用新特性(二)

Lambda 表达式

Lambda 表达式,实际上就是提供了一个类似匿名函数的特性,而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。

Lambda 表达式的基本语法如下:

[ caputrue ] ( params ) opt -> ret { body; };
  1. capture是捕获列表;

  2. params是参数表;(选填)

  3. opt是函数选项;可以填mutable,exception,attribute(选填)

  • mutable说明lambda表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获的对象的non-const方法。

  • exception说明lambda表达式是否抛出异常以及何种异常。

  • attribute用来声明属性。

  1. ret是返回值类型(拖尾返回类型)。(选填)
  2. body是函数体。

捕获列表:lambda表达式的捕获列表精细控制了lambda表达式能够访问的外部变量,以及如何访问这些变量。

  1. []不捕获任何变量。

  2. [&]捕获外部作用域中所有变量,并作为引用在函数体中使用(按引用捕获)。

  3. [=]捕获外部作用域中所有变量,并作为副本在函数体中使用(按值捕获)。注意值捕获的前提是变量可以拷贝,且被捕获的量在 lambda 表达式被创建时拷贝,而非调用时才拷贝。如果希望lambda表达式在调用时能即时访问外部变量,我们应当用引用方式捕获。

int a = 0;
auto f = [=] { return a; };

a+=1;

cout << f() << endl;       //输出0

int a = 0;
auto f = [&a] { return a; };

a+=1;

cout << f() <<endl;       //输出1
  1. [=,&foo]按值捕获外部作用域中所有变量,并按引用捕获foo变量。

  2. [bar]按值捕获bar变量,同时不捕获其他变量。

  3. [this]捕获当前类中的this指针,让lambda表达式拥有和当前类成员函数同样的访问权限。如果已经使用了&或者=,就默认添加此选项。捕获this的目的是可以在lamda中使用当前类的成员函数和成员变量。

class A
{
 public:
     int i_ = 0;

     void func(int x,int y){
         auto x1 = [] { return i_; };                   //error,没有捕获外部变量
         auto x2 = [=] { return i_ + x + y; };          //OK
         auto x3 = [&] { return i_ + x + y; };        //OK
         auto x4 = [this] { return i_; };               //OK
         auto x5 = [this] { return i_ + x + y; };       //error,没有捕获x,y
         auto x6 = [this, x, y] { return i_ + x + y; };     //OK
         auto x7 = [this] { return i_++; };             //OK

};

int a=0 , b=1;
auto f1 = [] { return a; };                         //error,没有捕获外部变量    
auto f2 = [&] { return a++ };                      //OK
auto f3 = [=] { return a; };                        //OK
auto f4 = [=] {return a++; };                       //error,a是以复制方式捕获的,无法修改
auto f5 = [a] { return a+b; };                      //error,没有捕获变量b
auto f6 = [a, &b] { return a + (b++); };                //OK
auto f7 = [=, &b] { return a + (b++); };                //OK

注意f4,虽然按值捕获的变量值均复制一份存储在lambda表达式变量中,修改他们也并不会真正影响到外部,但我们却仍然无法修改它们。如果希望去修改按值捕获的外部变量,需要显示指明lambda表达式为mutable。被mutable修饰的lambda表达式就算没有参数也要写明参数列表。

int a = 0;
auto f1 = [=] { return a++; };                //error
auto f2 = [=] () mutable { return a++; };       //OK

原因:lambda表达式可以说是就地定义仿函数闭包的“语法糖”。它的捕获列表捕获住的任何外部变量,最终会变为闭包类型的成员变量。按照C++标准,lambda表达式的operator()默认是const的,一个const成员函数是无法修改成员变量的值的。而mutable的作用,就在于取消operator()的const。

lambda表达式的大致原理:

每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,是一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。对于复制传值捕捉方式,类中会相应添加对应类型的非静态数据成员。在运行时,会用复制的值初始化这些成员变量,从而生成闭包。对于引用捕获方式,无论是否标记mutable,都可以在lambda表达式中修改捕获的值。至于闭包类中是否有对应成员,C++标准中给出的答案是:不清楚的,与具体实现有关。

lambda表达式是不能被赋值的:

auto a = [] { cout << "A" << endl; };
auto b = [] { cout << "B" << endl; };

a = b;   // 非法,lambda无法赋值
auto c = a;   // 合法,生成一个副本

闭包类型禁用了赋值操作符,但是没有禁用复制构造函数,所以你仍然可以用一个lambda表达式去初始化另外一个lambda表达式而产生副本。

在多种捕获方式中,最好不要使用[=]和[&]默认捕获所有变量

默认引用捕获所有变量,你有很大可能会出现悬挂引用(Dangling references),因为引用捕获不会延长引用的变量的生命周期:

std::function<int(int)> add_x(int x)
{
    return [&](int a) { return x + a; };
}

上面函数返回了一个lambda表达式,参数x仅是一个临时变量,函数add_x调用后就被销毁了,但是返回的lambda表达式却引用了该变量,当调用这个表达式时,引用的是一个垃圾值,会产生没有意义的结果。上面这种情况,使用默认传值方式可以避免悬挂引用问题。

但是采用默认值捕获所有变量仍然有风险,看下面的例子:

class Filter
{
public:
    Filter(int divisorVal):
        divisor{divisorVal}
    {}

    std::function<bool(int)> getFilter() 
    {
        return [=](int value) {return value % divisor == 0; };
    }

private:
    int divisor;
};

这个类中有一个成员方法,可以返回一个lambda表达式,这个表达式使用了类的数据成员divisor。而且采用默认值方式捕捉所有变量。你可能认为这个lambda表达式也捕捉了divisor的一份副本,但是实际上并没有。因为数据成员divisor对lambda表达式并不可见,你可以用下面的代码验证:

// 类的方法,下面无法编译,因为divisor并不在lambda捕捉的范围
std::function<bool(int)> getFilter() 
{
    return [divisor](int value) {return value % divisor == 0; };
}

原代码中,lambda表达式实际上捕捉的是this指针的副本,所以原来的代码等价于:

std::function<bool(int)> getFilter() {    return [this](int value) {return value % this->divisor == 0; };}

尽管还是以值方式捕获,但是捕获的是指针,其实相当于以引用的方式捕获了当前类对象,所以lambda表达式的闭包与一个类对象绑定在一起了,这很危险,因为你仍然有可能在类对象析构后使用这个lambda表达式,那么类似“悬挂引用”的问题也会产生。所以,采用默认值捕捉所有变量仍然是不安全的,主要是由于指针变量的复制,实际上还是按引用传值。

lambda表达式可以赋值给对应类型的函数指针。但是使用函数指针并不是那么方便。所以STL定义在< functional >头文件提供了一个多态的函数对象封装std::function,其类似于函数指针。它可以绑定任何类函数对象,只要参数与返回类型相同。如下面的返回一个bool且接收两个int的函数包装器:

std::function<bool(int, int)> wrapper = [](int x, int y) { return x < y; };

lambda表达式一个更重要的应用是其可以用于函数的参数,通过这种方式可以实现回调函数。

最常用的是在STL算法中,比如你要统计一个数组中满足特定条件的元素数量,通过lambda表达式给出条件,传递给count_if函数:

int value = 3;
vector<int> v {1, 3, 5, 2, 6, 10};
int count = std::count_if(v.beigin(), v.end(), [value](int x) { return x > value; });

再比如你想生成斐波那契数列,然后保存在数组中,此时你可以使用generate函数,并辅助lambda表达式:

vector<int> v(10);
int a = 0;
int b = 1;
std::generate(v.begin(), v.end(), 
              [&a, &b] { int value = b; b = b + a; a = value; return value; });
// 此时v {1, 1, 2, 3, 5, 8, 13, 21, 34, 55}

当需要遍历容器并对每个元素进行操作时:

std::vector<int> v = { 1, 2, 3, 4, 5, 6 };
int even_count = 0;
for_each(v.begin(), v.end(), [&even_count](int val){
    if(!(val & 1)){
        ++ even_count;
    }
});
std::cout << "The number of even is " << even_count << std::endl;

大部分STL算法,可以非常灵活地搭配lambda表达式来实现想要的效果。

C++ std::function

std::function是一个函数对象的包装器,std::function的实例可以存储,复制和调用任何可调用的目标,包括:

  1. 函数。

  2. lamada表达式。

  3. 绑定表达式或其他函数对象。

  4. 指向成员函数和指向数据成员的指针。

std::function对象没有初始化任何实际的可调用元素,调用std::function对象将抛出std::bad_function_call异常。

std::function简介

类模版std::function是一种通用、多态的函数封装。std::function的实例可以对任何可以调用的目标实体进行存储、复制、和调用操作,这些目标实体包括普通函数、Lambda表达式、函数指针、以及其它函数对象等。std::function对象是对C++中现有的可调用实体的一种类型安全的包裹(我们知道像函数指针这类可调用实体,是类型不安全的)。

通常std::function是一个函数对象类,它包装其它任意的函数对象,被包装的函数对象具有类型为T1, …,TN的N个参数,并且返回一个可转换到R类型的值。std::function使用 模板转换构造函数接收被包装的函数对象;特别是,闭包类型可以隐式地转换为std::function。

C++标准库详细说明了这个的基本使用http://www.cplusplus.com/reference/functional/function/.

这里我们大概总结一下。

Member types

成员类型说明
result_type返回类型
argument_type如果函数对象只有一个参数,那么这个代表参数类型。
first_argument_type如果函数对象有两个个参数,那么这个代表第一个参数类型。
second_argument_type如果函数对象有两个个参数,那么这个代表第二个参数类型。

Member functions

成员函数声明说明
constructor构造函数:constructs a new std::function instance
destructor析构函数: destroys a std::function instance
operator=给定义的function对象赋值
operator bool检查定义的function对象是否包含一个有效的对象
operator()调用一个对象

std::function使用

封装普通函数例子:

#include <iostream>#include <vector>#include <list>#include <map>#include <set>#include <string>#include <algorithm>#include <functional>#include <memory>using namespace std;typedef std::function<int(int)> Functional;int TestFunc(int a)  {      return a;  }int main(){    Functional obj = TestFunc;        int res = obj(1);    std::cout << res << std::endl;    while(1);    return 0;}

封装lambda表达式 :

#include <iostream>#include <vector>#include <list>#include <map>#include <set>#include <string>#include <algorithm>#include <functional>#include <memory>using namespace std;typedef std::function<int(int)> Functional;auto lambda = [](int a)->int{return a;};int main(){    Functional obj = lambda;        res = obj(2);    std::cout << res << std::endl;    while(1);    return 0;}

封装仿函数:

#include <iostream>#include <vector>#include <list>#include <map>#include <set>#include <string>#include <algorithm>#include <functional>#include <memory>using namespace std;typedef std::function<int(int)> Functional;class Functor{public:    int operator()(int a)    {        return a;    }};int main(){    Functor func;    Functional obj = func;    res = obj(3);    std::cout << res << std::endl;    while(1);    return 0;}

封装类的成员函数和static成员函数 :

#include <iostream>
#include <vector>
#include <list>
#include <map>
#include <set>
#include <string>
#include <algorithm>
#include <functional>
#include <memory>

using namespace std;

typedef std::function<int(int)> Functional;

class CTest
{
public:
    int Func(int a)
    {
        return a;
    }
    static int SFunc(int a)
    {
        return a;
    }
};

int main()
{
    CTest t;  
    obj = std::bind(&CTest::Func, &t, std::placeholders::_1);  
    res = obj(3);  
    cout << "member function : " << res << endl;  
  
    obj = CTest::SFunc;  
    res = obj(4);  
    cout << "static member function : " << res << endl;  

    while(1);
    return 0;
}

关于可调用实体转换为std::function对象需要遵守以下两条原则:

  1. 转换后的std::function对象的参数能转换为可调用实体的参数;
  2. 可调用实体的返回值能转换为std::function对象的返回值。

std::function对象最大的用处就是在实现函数回调,使用者需要注意,它不能被用来检查相等或者不相等,但是可以与NULL或者nullptr进行比较。

为什么要用std::function?

好用并实用的东西才会加入标准的。因为好用,实用,我们才在项目中使用它。std::function实现了一套类型消除机制,可以统一处理不同的函数对象类型。以前我们使用函数指针来完成这些;现在我们可以使用更安全的std::function来完成这些任务。

参考文档:

C++ std::function技术浅谈

右值引用和move语义

先看一个简单的例子直观感受下:

string a(x);                                    // line 1
string b(x + y);                                    // line 2
string c(some_function_returning_a_string());       // line 3

如果使用以下拷贝构造函数:

string(const string& that)
{
    size_t size = strlen(that.data) + 1;
    data = new char[size];
    memcpy(data, that.data, size);
}

以上3行中,只有第一行(line 1)的x深度拷贝是有必要的,因为我们可能会在后边用到x,x是一个左值(lvalues)。

第二行和第三行的参数则是右值,因为表达式产生的string对象是匿名对象,之后没有办法再使用了。

C++ 11引入了一种新的机制叫做“右值引用”,以便我们通过重载直接使用右值参数。我们所要做的就是写一个以右值引用为参数的构造函数:

string(string&& that)   // string&& is an rvalue reference to a string
{
data = that.data;
that.data = 0;
}

我们没有深度拷贝堆内存中的数据,而是仅仅复制了指针,并把源对象的指针置空。事实上,我们“偷取”了属于源对象的内存数据。由于源对象是一个右值,不会再被使用,因此客户并不会觉察到源对象被改变了。在这里,我们并没有真正的复制,所以我们把这个构造函数叫做“转移构造函数”(move constructor),他的工作就是把资源从一个对象转移到另一个对象,而不是复制他们。

有了右值引用,再来看看赋值操作符:

string& operator=(string that)
{
std::swap(data, that.data);
return *this;
}

注意到我们是直接对参数that传值,所以that会像其他任何对象一样被初始化,那么确切的说,that是怎样被初始化的呢?对于C++ 98,答案是复制构造函数,但是对于C++ 11,编译器会依据参数是左值还是右值在复制构造函数和转移构造函数间进行选择。

如果是a=b,这样就会调用复制构造函数来初始化that(因为b是左值),赋值操作符会与新创建的对象交换数据,深度拷贝。这就是copy and swap 惯用法的定义:构造一个副本,与副本交换数据,并让副本在作用域内自动销毁。这里也一样。

如果是a = x + y,这样就会调用转移构造函数来初始化that(因为x+y是右值),所以这里没有深度拷贝,只有高效的数据转移。相对于参数,that依然是一个独立的对象,但是他的构造函数是无用的(trivial),因此堆中的数据没有必要复制,而仅仅是转移。没有必要复制他,因为x+y是右值,再次,从右值指向的对象中转移是没有问题的。

总结一下:复制构造函数执行的是深度拷贝,因为源对象本身必须不能被改变。而转移构造函数却可以复制指针,把源对象的指针置空,这种形式下,这是安全的,因为用户不可能再使用这个对象了。

下面我们进一步讨论右值引用和move语义。

C98标准库中提供了一种唯一拥有性的智能指针std::auto_ptr该类型在C11中已被废弃,因为其“复制”行为是危险的。

auto_ptr<Shape> a(new Triangle);auto_ptr<Shape> b(a);

注意b是怎样使用a进行初始化的,它不复制triangle,而是把triangle的所有权从a传递给了b,也可以说成“a 被转移进了b”或者“triangle被从a转移到了b”。

auto_ptr 的复制构造函数可能看起来像这样(简化):

auto_ptr(auto_ptr& source)   // note the missing const
{
p = source.p;
source.p = 0;   // now the source no longer owns the object
}

auto_ptr 的危险之处在于看上去应该是复制,但实际上确是转移。调用被转移过的auto_ptr 的成员函数将会导致不可预知的后果。所以你必须非常谨慎的使用auto_ptr ,如果他被转移过。

auto_ptr<Shape> make_triangle()
{
    return auto_ptr<Shape>(new Triangle);
}

auto_ptr<Shape> c(make_triangle());      // move temporary into c
double area = make_triangle()->area();   // perfectly safe

auto_ptr<Shape> a(new Triangle);    // create triangle
auto_ptr<Shape> b(a);               // move a into b
double area = a->area();                // undefined behavior

显然,在持有auto_ptr 对象的a表达式和持有调用函数返回的auto_ptr值类型的make_triangle()表达式之间一定有一些潜在的区别,每调用一次后者就会创建一个新的auto_ptr对象。这里a 其实就是一个左值(lvalue)的例子,而make_triangle()就是右值(rvalue)的例子。

转移像a这样的左值是非常危险的,因为我们可能调用a的成员函数,这会导致不可预知的行为。另一方面,转移像make_triangle()这样的右值却是非常安全的,因为复制构造函数之后,我们不能再使用这个临时对象了,因为这个转移后的临时对象会在下一行之前销毁掉。

我们现在知道转移左值是十分危险的,但是转移右值却是很安全的。如果C++能从语言级别支持区分左值和右值参数,我就可以完全杜绝对左值转移,或者把转移左值在调用的时候暴露出来,以使我们不会不经意的转移左值。

C++ 11对这个问题的答案是右值引用。右值引用是针对右值的新的引用类型,语法是X&&。以前的老的引用类型X& 现在被称作左值引用。

使用右值引用X&&作为参数的最有用的函数之一就是转移构造函数X::X(X&& source),它的主要作用是把源对象的本地资源转移给当前对象。

C++ 11中,std::auto_ptr< T >已经被std::unique_ptr< T >所取代,后者就是利用的右值引用。

其转移构造函数:

unique_ptr(unique_ptr&& source)   // note the rvalue reference
{
    ptr = source.ptr;
    source.ptr = nullptr;
}

这个转移构造函数跟auto_ptr中复制构造函数做的事情一样,但是它却只能接受右值作为参数。

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);                 // error
unique_ptr<Shape> c(make_triangle());       // okay

第二行不能编译通过,因为a是左值,但是参数unique_ptr&& source只能接受右值,这正是我们所需要的,杜绝危险的隐式转移。第三行编译没有问题,因为make_triangle()是右值,转移构造函数会将临时对象的所有权转移给对象c,这正是我们需要的。

转移左值

有时候,我们可能想转移左值,也就是说,有时候我们想让编译器把左值当作右值对待,以便能使用转移构造函数,即便这有点不安全。出于这个目的,C++ 11在标准库的头文件< utility >中提供了一个模板函数std::move。实际上,std::move仅仅是简单地将左值转换为右值,它本身并没有转移任何东西。它仅仅是让对象可以转移。

以下是如何正确的转移左值:

unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a);              // still an error
unique_ptr<Shape> c(std::move(a));   // okay

请注意,第三行之后,a不再拥有Triangle对象。不过这没有关系,因为通过明确的写出std::move(a),我们很清楚我们的意图:亲爱的转移构造函数,你可以对a做任何想要做的事情来初始化c;我不再需要a了,对于a,您请自便。

当然,如果你在使用了mova(a)之后,还继续使用a,那无疑是搬起石头砸自己的脚,还是会导致严重的运行错误。

总之,std::move(some_lvalue)将左值转换为右值(可以理解为一种类型转换),使接下来的转移成为可能。

一个例子:

class Foo
{
    unique_ptr<Shape> member;

public:
    Foo(unique_ptr<Shape>&& parameter)
    : member(parameter)   // error
    {}

};

上面的parameter,其类型是一个右值引用,只能说明parameter是指向右值的引用,而parameter本身是个左值。(Things that are declared as rvalue reference can be lvalues or rvalues. The distinguishing criterion is: if it has a name, then it is an lvalue. Otherwise, it is an rvalue.)

因此以上对parameter的转移是不允许的,需要使用std::move来显示转换成右值。

0

评论区