C++11的一些新特性(&&、move、forward、emplace_back)

右值引用与移动:

C++11中引用了右值引用和移动语义,可以避免无谓的复制,提高了程序性能。

区分表达式的左右值属性:如果可对表达式用&符取址,则为左值,否则为右值。

在进入正题之前,首先得区分什么是左右值:

左值lvalue 是有标识符、可以取地址的表达式,最常见的情况有:
**变量、函数或数据成员的名字
**返回左值引用的表达式,如 ++x、x = 1、cout << ' '
**字符串字面量如 "hello world"
 
右值prvalue 是没有标识符、不可以取地址的表达式,一般也称之为“临时对象”。最常见的情况有:
**返回非引用类型的表达式,如 x++、x + 1、make_shared(42)
**除字符串字面量之外的字面量,如 42、true

C++11为我们提供了一种能引用右值的办法:

1. &&:

&&:        左右值都可以引用

&  :        肯定是左值引用 

        右值引用就是对一个右值进行引用的类型。因为右值没有名字,所以我们只能通过引用的方式找到它。无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所把绑定对象的内存,只是该对象的一个别名。

特点如下:

1. 左值和右值是独立于它们的类型的,右值引用类型可能是左值也可能是右值。 
2. auto&& 或函数参数类型自动推导的 T&& 是一个未定的引用类型,被称为 universal     references,它可能是左值引用也可能是右值引用类型,取决于初始化的值类型。
3. 所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用折叠都为左值引用。当 T&&      为模板参数时,输入左值,它会变成左值引用,而输入右值时则变为具名的右值引用。 
4. 编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值。 

 特点理解起来不是很容易,先看一下右值引用

#include <iostream>
using namespace std;

class A
{
public:
    A() :m_ptr(new int(0)) {
        cout << "constructor A"  << endl;
    }
    ~A(){
        cout << "destructor A, m_ptr:" << m_ptr  << endl;
        delete m_ptr;
        m_ptr = nullptr;
    }

private:
    int* m_ptr;
};

// 为了避免返回值优化(删除保持函数返回值的临时对象),此函数故意这样写???
A Get(bool flag)
{
    A a;
    A b;
    cout << "ready return" << endl;
    if (flag)
        return a;
    else
        return b;
}

int main()
{
    {
        A a = Get(false); 
    }
    cout << "main finish" << endl;
    return 0;
}

        在上面的代码中,默认构造函数是浅拷贝(浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。),main函数的 a 和Get函数的 b 会指向同一个指针 m_ptr,在析构的时候会导致重复删除该指针。

          在看懂这个代码情况下,运行这个代码,观察拷贝的过程:

         可以发现这里比理论上的destructor次数多一次,说明我们构造的指针比容器数量多,存在多个指针指向同一个内存,所以为了每一个指针拥有一个独立的指针,我们构造函数改为这样,其他不变:

A() :m_ptr(new int(0)) {
        cout << "constructor A"  << endl;
   }
    A(const A& a) :m_ptr(new int(*a.m_ptr)) {
        cout << "copy constructor A"  << endl;
   }    //保证复制是深度复制

         改进之后,这样就可以保证拷贝构造时的安全性,但有时这种拷贝构造却是不必要的,比如上面代码中的拷贝构造就是不必要的。上面代码中的 Get 函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象 b,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。有没有办法避免临时对象的拷贝构造呢?答案是肯定的。看下面的代码:

        

   A() :m_ptr(new int(0)) {
        cout << "constructor A"  << endl;
   }
    A(const A& a) :m_ptr(new int(*a.m_ptr)) {
        cout << "copy constructor A"  << endl;
   }
    A(A&& a) :m_ptr(a.m_ptr) {//A&& 用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数
        a.m_ptr = nullptr;
        cout << "move constructor A"  << endl;

       上面的代码中没有了拷贝构造,取而代之的是移动构造( Move Construct)。从移动构造函数的实现中可以看到,它的参数是一个右值引用类型的参数 A&&,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。这里的 A&& 用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义( move 语义),右值引用的一个重要目的是用来支持移动语义的。

移动语义:

 1. move/forward:

move:        左值变右值

forward:    (1)不是(2)传入,就是左值变右值

                  (2)T &&t    根据原来的值判断。    

#include <iostream>
#include <vector>
#include <cstdio>
#include <cstdlib>
#include <string.h>
using namespace std;

class MyString {
private:
    char* m_data;
    size_t   m_len;
    void copy_data(const char *s) {
        m_data = new char[m_len+1];
        memcpy(m_data, s, m_len);
        m_data[m_len] = '\0';
    }
public:
    MyString() {
        m_data = NULL;
        m_len = 0;
    }

    MyString(const char* p) {
        m_len = strlen (p);
        copy_data(p);
    }

    MyString(const MyString& str) {
        m_len = str.m_len;
        copy_data(str.m_data);
        std::cout << "Copy Constructor is called! source: " << str.m_data << std::endl;
    }
    MyString& operator=(const MyString& str) {
        if (this != &str) {
            m_len = str.m_len;
            copy_data(str.m_data);
        }
        std::cout << "Copy Assignment is called! source: " << str.m_data << std::endl;
        return *this;
    }

    // 用c++11的右值引用来定义这两个函数
    MyString(MyString&& str) {
        std::cout << "Move Constructor is called! source: " << str.m_data << std::endl;
        m_len = str.m_len;
        m_data = str.m_data; //避免了不必要的拷贝
        str.m_len = 0;
        str.m_data = NULL;
    }

    MyString& operator=(MyString&& str) {
        std::cout << "Move Assignment is called! source: " << str.m_data << std::endl;
        if (this != &str) {
            m_len = str.m_len;
            m_data = str.m_data; //避免了不必要的拷贝
            str.m_len = 0;
            str.m_data = NULL;
        }
        return *this;
    }

    virtual ~MyString() {
        if (m_data) free(m_data);
    }
};


int main()
{
    MyString a;
    a = MyString("Hello");      // move
    MyString b = a;             // copy
    MyString c = std::move(a);  // move, 将左值转为右值
    return 0;
}

        移动语义是通过右值引用来匹配临时值的,那么,普通的左值是否也能借组移动语义来优化性能呢?C++11为了解决这个问题,提供了std::move()方法来将左值转换为右值,从而方便应用移动语义。move是将对象的状态或者所有权从一个对象转移到另一个对象,只是转义,没有内存拷贝。

———————————————————————————————————————————

        forward 完美转发实现了参数在传递过程中保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值(用途比move广)。

int &&a = 10;
int &&b = a; //错误

int &&a = 10;
int &&b = std::forward<int>(a);

减少内存拷贝和移动的应用:

        利用减少拷贝和移动的原理,实现了一些优化之后的函数:

1. emplace_back

        emplace_back是就地构造,不用构造后再次复制到容器中。因此效率更高。

测试用例:

time_interval.h

#ifndef TIME_INTERVAL_H
#define TIME_INTERVAL_H
#include <iostream>
#include <memory>
#include <string>
#ifdef GCC
#include <sys/time.h>
#else
#include <ctime>
#endif // GCC

class TimeInterval
{
public:
    TimeInterval(const std::string& d) : detail(d)
    {
        init();
    }

    TimeInterval()
    {
        init();
    }

    ~TimeInterval()
    {
#ifdef GCC
        gettimeofday(&end, NULL);
        std::cout << detail
                  << 1000 * (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec) / 1000
                  << " ms" << endl;
#else
        end = clock();
        std::cout << detail
                  << (double)(end - start) << " ms" << std::endl;
#endif // GCC
    }

protected:
    void init() {
#ifdef GCC
        gettimeofday(&start, NULL);
#else
        start = clock();
#endif // GCC
    }
private:
    std::string detail;
#ifdef GCC
    timeval start, end;
#else
    clock_t start, end;
#endif // GCC
};
#define TIME_INTERVAL_SCOPE(d)   std::shared_ptr<TimeInterval> time_interval_scope_begin = std::make_shared<TimeInterval>(d)
#endif // TIME_INTERVAL_H

main.cpp:

#include <vector>
#include <string>
#include "time_interval.h"

int main() {


    std::vector<std::string> v;
    int count = 10000000;
    v.reserve(count);       //预分配十万大小,排除掉分配内存的时间
    {
        TIME_INTERVAL_SCOPE("push_back string:");
        for (int i = 0; i < count; i++)
        {
            std::string temp("ceshi");
            v.push_back(temp);// push_back(const string&),参数是左值引用
        }
    }

    v.clear();
    {
        TIME_INTERVAL_SCOPE("push_back move(string):");
        for (int i = 0; i < count; i++)
        {
            std::string temp("ceshi");
            v.push_back(std::move(temp));// push_back(string &&), 参数是右值引用
        }
    }

    v.clear();
    {
        TIME_INTERVAL_SCOPE("push_back(string):");
        for (int i = 0; i < count; i++)
        {
            v.push_back(std::string("ceshi"));// push_back(string &&), 参数是右值引用
        }
    }

    v.clear();
    {
        TIME_INTERVAL_SCOPE("push_back(c string):");
        for (int i = 0; i < count; i++)
        {
            v.push_back("ceshi");// push_back(string &&), 参数是右值引用
        }
    }

    v.clear();
    {
        TIME_INTERVAL_SCOPE("emplace_back(c string):");
        for (int i = 0; i < count; i++)
        {
            v.emplace_back("ceshi");// 只有一次构造函数,不调用拷贝构造函数,速度最快
        }
    }
}

可以得到:

可以看出,我们在建立程序时,应该优先考虑emplace_back函数。

 


版权声明:本文为weixin_43961989原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。