C++11实用特性2

1 可调用对象包装器、绑定器

1可调用对象

C++中的可调用对象分为四类:

  • 函数指针:
    任何一个函数都可以抽象成一个函数指针
int print(int a, double b)
{
    cout << a << b << endl;
    return 0;
}
// 定义函数指针
int (*func)(int, double) = &print;

 

  • 仿函数(函数对象)
    如果一个对象具有operator()成员函数,那么就称其为仿函数
#include <iostream>
#include <string>
#include <vector>
using namespace std;

struct Test
{
    // ()操作符重载
    void operator()(string msg)
    {
        cout << "msg: " << msg << endl;
    }
};

int main(void)
{
    Test t;
    t("我是要成为海贼王的男人!!!");	// 仿函数
    return 0;
}

 

  • 类的静态函数
  • 可转为函数指针的类对象(比较麻烦难懂,不推荐)
    利用operator可将类对象转换为函数指针,需要在类内定义operator 函数指针(),括号内只能为空,返回值要求是一个函数地址,且这个函数只能是属于类的静态成员函数;而此时就相当于调用这个对象并传入对应函数所需参数时,对应的函数就被调用
     
  • 类成员函数指针、类成员变量指针(不好用)
    定义非静态成员函数的函数指针时要加上类作用域才行,因为非静态成员函数在对象未分配前不存在地址
using fptr = void(Test::*) (int, string)//后面是参数列表,表明是属于Test类中的一个指针

2 可调用对象包装器function

function的出现使可调用对象变成一种统一的类型来使用,其使用语法如下:

#include <functional>
std::function<返回值类型(参数类型列表)> diy_name = 可调用对象;

要注意类成员函数指针是不能直接打包的,要借助bind才行

3 可调用对象绑定器

std::bind用来将可调用对象与其参数一起进行绑定。绑定后的结果可以使用std::function进行保存,并延迟调用到任何我们需要的时候。通俗来讲,它主要有两大作用:

  • 将可调用对象与其参数一起绑定成一个仿函数
  • 将多元(参数个数为n,n>1)可调用对象转换为一元或者(n-1)元可调用对象,即只绑定部分参数。

其使用语法如下:

// 绑定非类成员函数/变量,静态成员函数
auto f = std::bind(可调用对象地址, 绑定的参数/占位符);
// 绑定类成员函/变量
auto f = std::bind(类函数/成员地址, 类实例对象地址, 绑定的参数/占位符);
指定第一个参数的时候加上类作用域

2 lambda表达式

1 基本语法

lambda表达式定义了一个匿名函数,并且可以捕获一定范围内的变量。其最大的好处就是使用时定义,lambda表达式的语法形式简单归纳如下:

[capture](params) opt -> ret {body;};
- capture:捕获列表,对函数体外部变量的捕获方式
      [] - 不捕捉任何变量
      [&] - 捕获外部作用域中所有变量, 并作为引用在函数体内使用 (按引用捕获)
      [=] - 捕获外部作用域中所有变量, 并作为副本在函数体内使用 (按值捕获)
            拷贝的副本在匿名函数体内部是只读的
      [=, &foo] - 按值捕获外部作用域中所有变量, 并按照引用捕获外部变量 foo
      [bar] - 按值捕获 bar 变量, 同时不捕获其他变量
      [&bar] - 按引用捕获 bar 变量, 同时不捕获其他变量
      [this] - 捕获当前类中的this指针
               让lambda表达式拥有和当前类成员函数同样的访问权限
               如果已经使用了 & 或者 =, 默认添加此选项
               但是只能使用类中的成员变量
- params:参数列表,和普通函数参数列表一样,没有可以不写
- opt:函数选项,没有可以不写
      - mutable: 可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)
      - exception: 指定函数抛出的异常,如抛出整数类型的异常,可以使用throw();
- ret:返回值类型
- body:函数体

使用示例:
要注意匿名函数的大括号后面加上一个小括号才算调用

void func(int x, int y){
    int a = 7;
    int b = 9;
    [=, &x](){
        int c = a;
        int d = x;
        x++;
    }();
    cout<<x;
}

int main()
{
    func(1,2);
    return 0;
}

2 返回值

很多时候,lambda表达式的返回值是非常明显的,因此在C++11中允许省略lambda表达式的返回值。

// 完整的lambda表达式定义
auto f = [](int a) -> int
{
    return a+10;  
};

// 忽略返回值的lambda表达式定义
auto f = [](int a)
{
    return a+10;  
};

3 本质

lambda表达式的类型在C++11中会被看做是一个带operator()的类,即仿函数。
按照C++标准,lambda表达式的operator()默认是const的,一个const成员函数是无法修改成员变量值的。

mutable选项的作用就在于取消operator()的const属性。

由于lambda表达式在C++中会被看做是一个仿函数,因此可以使用std::function和std::bind来存储和操作lambda表达式:

#include <iostream>
#include <functional>
using namespace std;

int main(void)
{
    // 包装可调用函数
    std::function<int(int)> f1 = [](int a) {return a; };
    // 绑定可调用函数
    std::function<int(int)> f2 = bind([](int a) {return a; }, placeholders::_1);

    // 函数调用
    cout << f1(100) << endl;
    cout << f2(200) << endl;
    return 0;
}

3 右值引用

右值引用也是C++提供的新特性:

  • 左值(location value)
    指存储在内存中、有明确存储地址的数据,即可以取地址
  • 右值(readable value)
    可以提供数据值的数据,不可取地址,比如数字

首先明确,无论左值引用(&)还是右值引用(&&),它们的前提都是引用,就是个别名,而引用的实质就是个指针常量void* const

1 右值引用的作用

有时我们会通过一个临时对象来构造我们的对象,如果这个临时对象本身就很大,它创建出来已经耗费了一部分系统资源,然后还要再拷贝给我们自己的对象,拷贝完再销毁这个临时对象,整个过程的开销是很大的,我们就利用右值引用来延长这种临时对象的声明周期,使得可以直接使用它而不发生拷贝

2 移动构造函数

上面的问题就引出了类中的又一个构造函数--移动构造函数,其就是为了解决这种不必要的拷贝,实现复用其它对象中的资源(堆内存),这本质上是一种浅拷贝,但区别在于移动构造中要让原对象指向这片地址的指针指向空,使得该地址的管理权被新的对象独享,或者理解为移动构造函数把临时对象的指针指向移走了

class Test
{
public:
    Test(){
        
    }
    Test(Test&& a):m_num(a.m_num)
    {
        a.m_num = nullptr;
    }
private:
    int *m_num;
};

Test getRet(){
    Test t;
    return t;
}

int main()
{
    //getRet在返回的时候会创建一个临时对象接受t,
    Test t1 = getRet();
    Test &&t2 = getRet();
    return 0;
}

在添加了移动构造函数之后,对象赋值的时候编译器会判断等号右边的是否为临时对象,便选择哪一种构造函数来实现,如果没有移动构造函数,那么调用的就是拷贝构造函数了。

如今C++中的函数在返回一个类对象时,理论上我们会认为通过复制构造函数复制一个临时对象,然后在赋值给外部变量。实际上编译器做了优化,省去了中间的临时对象环节。所以有时我们无法看到移动构造被调用

3 &&

在C++中,并不是所有情况下 && 都代表是一个右值引用,具体的场景体现在模板和自动类型推导中,如果是模板参数需要指定为T&&,如果是自动类型推导需要指定为auto &&,在这两种场景下 &&被称作未定的引用类型,或者称之为万能引用。另外还有一点需要额外注意const T&&特指一个右值引用,不是未定引用类型。

template<typename T>
void f(T&& param);
void f1(const T&& param);
f(10); 	
int x = 10;
f(x); 
f1(x);	// error, x是左值
f1(10); // ok, 10是右值

由于上述代码中存在T&&或者auto&&这种未定引用类型,当它作为参数时,有可能被一个右值引用初始化,也有可能被一个左值引用初始化,在进行类型推导时右值引用类型(&&)会发生变化,这种变化被称为引用折叠。在C++11中引用折叠的规则如下:

  • 通过右值推导 T&& 或者 auto&& 得到的是一个右值引用类型

  • 通过非右值(右值引用、左值、左值引用、常量右值引用、常量左值引用)推导 T&& 或者 auto&& 得到的是一个左值引用类型

5 右值变左值

#include <iostream>
using namespace std;

void printValue(int &i)
{
    cout << "l-value: " << i << endl;
}

void printValue(int &&i)
{
    cout << "r-value: " << i << endl;
}

void forward(int &&k)
{
    printValue(k);
}

int main()
{
    int i = 520;
    printValue(i);
    printValue(1314);
    forward(250);

    return 0;
}

根据测试代码可以得知,编译器会根据传入的参数的类型(左值还是右值)调用对应的重置函数(printValue),函数forward()接收的是一个右值,但是在这个函数中调用函数printValue()时,参数k变成了一个命名对象,编译器会将其当做左值来处理。

4 转移和完美转发

1 转移move

在C++11添加了右值引用,并且不能使用左值初始化右值引用,如果想要使用左值初始化一个右值引用需要借助std::move()函数,使用std::move方法可以将左值转换为右值。使用这个函数并不能移动任何东西,而是和移动构造函数一样都具有移动语义,将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝,而这个左值被转移了控制权以后就变成了将亡值

语法:

template<class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) _NOEXCEPT
{	// forward _Arg as movable
    return (static_cast<remove_reference_t<_Ty>&&>(_Arg));
}

由于一个右值在绑定过后再次使用时会被当作左值,所以需要用move来保持它的右值属性

int &&a = 10;
int &&b = move(a);//去掉move会报错,左值不能给右值赋值

使用场景:

  • 用左值对右值初始化
  • 一个对象不再使用,但是要拷贝它的数据到另一个对象中,用move来避免拷贝

2 forward

一个右值引用作为函数参数的形参时,在函数内部转发该参数给内部其他函数时,它就变成一个左值,并不是原来的类型了。forward用于保证引用的类型在传递过程中不会发生变化,其使用原型std::forward(t);

  • 当T为左值引用类型时,t将被转换为T类型的左值
  • 当T不是左值引用类型时,t将被转换为T类型的右值
#include <iostream>
using namespace std;

template<typename T>
void printValue(T& t)
{
    cout << "l-value: " << t << endl;
}

template<typename T>
void printValue(T&& t)
{
    cout << "r-value: " << t << endl;
}

template<typename T>
void testForward(T && v)
{
    printValue(v);
    printValue(move(v));
    printValue(forward<T>(v));
    cout << endl;
}

int main()
{
    testForward(520);
    int num = 1314;
    testForward(num);
    testForward(forward<int>(num));
    testForward(forward<int&>(num));
    testForward(forward<int&&>(num));

    return 0;
}
posted @ 2023-08-10 15:19  白日梦想家-c  阅读(8)  评论(0编辑  收藏  举报