C++ 面试 C++ 11 新特性之右值引用与移动

右值引用

什么是左值,什么是右值,简单说左值可以赋值,右值不可以赋值。以下面代码为例,“ A a = getA();”该语句中a是左值,getA()的返回值是右值。

#include <iostream>

class A
{
public:
    A() { std::cout << "Constructor" << std::endl; }
    A(const A&) { std::cout << "Copy Constructor" << std::endl; }
    ~A() {}
};

static A getA()
{
    A a;
    return a;
}

int main()
{
    A a = getA();

    return 0;
}

运行以上代码,输出结果如下:

Constructor
Copy Constructor

可以看到A的构造函数调用一次,拷贝构造函数调用了一次,构造函数和拷贝构造函数是消耗比较大的,这里是否可以避免拷贝构造?

C++11做到了这一点。

#include <iostream>

class A
{
public:
    A() { std::cout << "Constructor" << std::endl; }
    A(const A&) { std::cout << "Copy Constructor" << std::endl; }
    A(const A&&) { std::cout << "Move Constructor" << std::endl; }
    ~A() {}
};

static A getA()
{
    A a;
    return a;
}

int main()
{
    A a = getA();

    return 0;
}

运行以上代码,输出结果:

Constructor
Move Constructor

这样就没有调用拷贝构造函数,而是调用移动构造。这里并没有看到移动构造的优点。

#include <iostream>
#include <vector>

class B
{
public:
    B() {}
    B(const B&) { std::cout << "B Constructor" << std::endl; }
};

class A
{
public:
    A(): m_b(new B()) { std::cout << "A Constructor" << std::endl; }
    A(const A& src) :
        m_b(new B(*(src.m_b)))
    { 
        std::cout << "A Copy Constructor" << std::endl;
    }
    A(A&& src) :
        m_b(src.m_b)
    {
        src.m_b = nullptr;
        std::cout << "A Move Constructor" << std::endl;
    }
    ~A() { delete m_b; }

private:
    B* m_b;
};

static A getA()
{
    A a;
    std::cout << "================================================" << std::endl;
    return a;
}

int main()
{
    A a = getA();
    std::cout << "================================================" << std::endl;
    A a1(a);

    return 0;
}

输出:

A Constructor
================================================
A Move Constructor
================================================
B Constructor
A Copy Constructor

“ A a = getA();”调用的是A的移动构造,“ A a1(a); ”调用的是A的拷贝构造。A的拷贝构造需要对成员变量B进行深拷贝,而A的移动构造不需要,很明显,A的移动构造效率高。

std::move

std::move语句可以将左值变为右值而避免拷贝构造,修改代码如下:

#include <iostream>
#include <vector>

class B
{
public:
    B() {}
    B(const B&) { std::cout << "B Constructor" << std::endl; }
};

class A
{
public:
    A(): m_b(new B()) { std::cout << "A Constructor" << std::endl; }
    A(const A& src) :
        m_b(new B(*(src.m_b)))
    { 
        std::cout << "A Copy Constructor" << std::endl;
    }
    A(A&& src) noexcept :
        m_b(src.m_b)
    {
        src.m_b = nullptr;
        std::cout << "A Move Constructor" << std::endl;
    }
    ~A() { delete m_b; }

private:
    B* m_b;
};

static A getA()
{
    A a;
    std::cout << "================================================" << std::endl;
    return a;
}

int main()
{
    A a = getA();
    std::cout << "================================================" << std::endl;
    A a1(a);
    std::cout << "================================================" << std::endl;
    A a2(std::move(a1));
    return 0;
}

运行以上代码,输出结果:

A Constructor
================================================
A Move Constructor
================================================
B Constructor
A Copy Constructor
================================================
A Move Constructor

“ A a2(std::move(a1));”将a1转换为右值,因此a2调用的移动构造而不是拷贝构造。

#include <iostream>
#include <vector>


class B
{
public:
    B() {}
    B(const B&) { std::cout << "B Constructor" << std::endl; }
};

class A
{
public:
    A(): m_b(new B()) { std::cout << "A Constructor" << std::endl; }
    A(const A& src) :
        m_b(new B(*(src.m_b)))
    { 
        std::cout << "A Copy Constructor" << std::endl;
    }
    A(A&& src) :
        m_b(src.m_b)
    {
        src.m_b = nullptr;
        std::cout << "A Move Constructor" << std::endl;
    }
    A& operator=(const A& src) noexcept
    {
        if (this == &src)
            return *this;

        delete m_b;
        m_b = new B(*(src.m_b));
        std::cout << "operator=(const A& src)" << std::endl;
        return *this;
    }
    A& operator=(A&& src) noexcept
    {
        if (this == &src)
            return *this;

        delete m_b;
        m_b = src.m_b;
        src.m_b = nullptr;
        std::cout << "operator=(const A&& src)" << std::endl;
        return *this;
    }
    ~A() { delete m_b; }

private:
    B* m_b;
};

static A getA()
{
    A a;
    std::cout << "================================================" << std::endl;
    return a;
}

int main()
{
    A a = getA();//移动构造
    std::cout << "================================================" << std::endl;
    A a1(a);//拷贝构造
    std::cout << "================================================" << std::endl;
    A a2(std::move(a1));//移动构造
    std::cout << "================================================" << std::endl;
    a2 = getA();//移动赋值
    std::cout << "================================================" << std::endl;
    a2 = a1;//拷贝赋值
    return 0;
}

输出结果:

A Constructor
================================================
A Move Constructor
================================================
B Constructor
A Copy Constructor
================================================
A Move Constructor
================================================
A Constructor
================================================
A Move Constructor
operator=(const A&& src)
================================================
B Constructor
operator=(const A& src)

总之尽量给类添加移动构造和移动赋值函数,而减少拷贝构造和拷贝赋值的消耗。 移动构造,移动赋值要加上noexcept,用于通知标准库不抛出异常。

如何评价 C++11 的右值引用(Rvalue reference)特性?

一般性:

右值引用

右值是一个行将销毁的值,例如(i * 10)这种表达式的值。新标准中允许通过&&标识定义一个右值引用,将其绑定到一个右值上。但是,一个右值引用变量又是一个左值,因为它是一个变量了嘛。

std::cout<<"test rvalue reference:\n";
int j = 42;
int &lr = j;
//int &&rr = j; // Wrong. Can't bind a rvalue ref to a lvalue.
//int &lr2 = i * 42; // Wrong. Can't bind a lvalue ref to a rvalue.
const int &lr3 = j * 42;
int &&rr2 = j * 42;
//int &&rr3 = rr2; // Wrong. rr2 is a rvalue ref and rvalue ref is a lvalue.
int &lr4 = rr2;
std::cout<<j<<'\t'<<lr<<'\t'<<lr3<<'\t'<<rr2<<'\t'<<lr4<<std::endl;
std::cout<<"test rvalue ref done.\n"<<std::endl;

std::move

std::move函数的作用很简单,就是获得一个左值的右值引用,这样我们就找到了一种途径将一个右值引用绑定到一个左值上。

但是,使用std::move也意味着交出左值的控制权,之后就不能再使用这个左值了,因为使用std::move之后,无法对这个左值做任何保证。

std::cout<<"test std::move:\n";
std::string str5 = "asdf";
std::string &lr5 = str5;
std::string &&rr5 = std::move(str5);
rr5[0] = 'b';
lr5[1] = 'z';
std::cout<<rr5<<'\t'<<lr5<<'\t'<<str5<<std::endl;
std::cout<<"test std::move done.\n"<<std::endl;

移动构造

新标准中一些内置类型(如string)都实现了移动构造函数。所谓移动构造,就是接受一个右值引用,从而接受该右值引用所引用的对象,而没有实际的大块内存拷贝操作(可以想象成只拷贝了一个指针而不是整块的内存)。调用移动构造函数的关键是要传入一个相应的右值引用,这时上面提到的std::move函数就派上用场了。

std::cout<<"test move constructor:\n";
std::allocator<std::string> alloc;
size_t size = 5;
auto old_strs = alloc.allocate(size);
for(size_t i = 0; i < size; i++)
{
    alloc.construct(old_strs + i, "abcde");
}
std::cout<<"old_strs[0]: "<<old_strs[0]<<std::endl;
auto new_strs = alloc.allocate(size);
for(size_t i = 0; i < size; i++)
{
    alloc.construct(new_strs + i, std::move(*(old_strs + i)));
}
std::cout<<"new_strs[0]: "<<new_strs[0]<<std::endl;
std::cout<<"old_strs[0]: "<<old_strs[0]<<std::endl;
for(size_t i = 0; i < size; i++)
{
    alloc.destroy(old_strs + i);
}
alloc.deallocate(old_strs, size);
std::cout<<"test move constructor done.\n"<<std::endl;

调用移动构造函数之后,右值引用所绑定的对象保证可析构可销毁的状态。

定义自己的移动构造函数

上面说到了,移动构造函数的关键是接受一个右值引用,窃取该对象的内容为己所用(不拷贝),并且保证被窃取的对象保持可析构可销毁的状态。那么,我们当然可以定义一个自己的移动构造函数。

一个整型数组的定义如下:

class IntVec
{
public:
    IntVec() = default;
    IntVec(size_t capacity);
    IntVec(IntVec &rhs);
    IntVec(IntVec &&rhs) noexcept;
    IntVec &operator=(IntVec &&rhs) & noexcept;
    ~IntVec();

    int push_back(int val);
    void print_info();

    size_t capacity;
    size_t size;
    int *pointer;
};
IntVec::IntVec(IntVec &rhs)
{
    this->capacity = rhs.capacity;
    this->size = rhs.size;
    this->pointer = new int[this->capacity];
    for(size_t i = 0; i < size; i++)
        this->pointer[i] = rhs.pointer[i];
    std::cout<<"IntVect copy constructor.\n";
}

IntVec::IntVec(size_t capacity)
    : capacity(capacity), size(0)
{
    this->pointer = new int[capacity];
}

IntVec::IntVec(IntVec &&rhs) noexcept
    : capacity(rhs.capacity), size(rhs.size), pointer(rhs.pointer)
{
        rhs.pointer = nullptr;
        rhs.capacity = rhs.size = 0;
        std::cout<<"IntVect move constructor.\n";
}

IntVec &IntVec::operator=(IntVec &&rhs) & noexcept
{
    if(this != &rhs)
    {
        if(this->pointer)
            delete [] this->pointer;
        this->pointer = rhs.pointer;
        this->capacity = rhs.capacity;
        this->size = rhs.size;
        rhs.pointer = nullptr;
        rhs.capacity = rhs.size = 0;
    }
    std::cout<<"IntVect move assign constructor.\n";
    return *this;
}
IntVec::~IntVec()
{
    if(this->pointer)
        delete [] this->pointer;
}

push_back和print_info的定义就不赘述了。

可以看到,在移动构造函数里,只需要窃取指针及其状态,并将右值引用对象的状态重置,即可完成移动构造的操作。

同样的,我们还可以定义移动赋值运算。

值得注意的是,两个移动函数都添加了noexcept标识符。这也是C++11新标准中引入的,用于向标准库指明此函数不会抛出异常,以避免标准库在和我们定义的这个类进行交互时做一些不必要的工作。如果我们不承诺noexcept,那么当标准库容器扩展容量时,就不能调用移动构造函数来移动容器内的现存元素,而只能采取比较耗费资源的拷贝构造函数。

这一部分的测试代码如下:

std::cout<<"test custom move copy constructor/move assign operator.\n";
IntVec iv1(10);
for(size_t i = 0; i < 5; i++)
    iv1.push_back(i);
std::cout<<"-------iv1:\n";
iv1.print_info();

IntVec iv2(std::move(iv1));
std::cout<<"-------iv2:\n";
iv2.print_info();
std::cout<<"-------iv1:\n";
iv1.print_info();

IntVec iv3 = iv2;
std::cout<<"-------iv3:\n";
iv3.print_info();
std::cout<<"-------iv2:\n";
iv2.print_info();

IntVec iv4(5);
std::cout<<"-------iv4:\n";
iv4.print_info();
iv4 = std::move(iv2);
std::cout<<"-------iv4:\n";
iv4.print_info();
std::cout<<"-------iv2:\n";
iv2.print_info();

std::cout<<"test custom move copy constructor/move assign operator done.\n"<<std::endl;

移动迭代器

新标准中提供了std::make_move_iterator函数用于从普通迭代器获得移动迭代器。对移动迭代器解引用将会获得对应的右值引用,从而方便的对整个容器进行移动操作。

std::cout<<"test move iterator:\n";
auto new_strs2 = alloc.allocate(size);
std::uninitialized_copy(std::make_move_iterator(new_strs),
        std::make_move_iterator(new_strs + size),
        new_strs2);
std::cout<<"new_strs[0]: "<<new_strs[0]<<std::endl;
std::cout<<"new_strs2[0]: "<<new_strs2[0]<<std::endl;
for(size_t i = 0; i < size; i++)
{
    alloc.destroy(new_strs + i);
}
alloc.deallocate(new_strs, size);
std::cout<<"test move iterator done.\n"<<std::endl;

引用折叠规则

当左右引用遇到模板参数的时候,需要用到引用折叠规则来获得最终的模板推断类型和形参类型。

template <typename T>
void vague_func(T&& val)
{
    std::cout<<"val: "<<val<<std::endl;
    T val2 = val;
    val2++;
    std::cout<<"val2: "<<val2<<'\t'<<"val: "<<val<<std::endl;
}

std::cout<<"test ref folding:\n";
int val = 2;
int &lref = val;
int &&rref = 2;
std::cout<<"-------with val:\n";
vague_func(2);
std::cout<<"-------with lref:\n";
vague_func(lref);
std::cout<<"-------with rref:\n";
vague_func(rref);
vague_func(std::move(val));
std::cout<<"test ref done.\n"<<std::endl;

在上述vague_func中,虽然val的类型是T&&,看上去是个右值引用,但是实际上也是可以接受左值引用的类型的。当传入一个左值时,如lref,编译器会推断T = int&而不是T = int。那么这时实际实例化的vague_func实际是:

void vague_func(int& && val)

根据引用折叠规则,除了T&& &&折叠为T&&之外的所有情况均折叠为T&,那么最终vague_func为:

void vague_func(int& val)

因此,vague_func也可以接受一个左值实参。这种引用折叠规则,也是std::move得以实现的基础,有兴趣的读者可以自行去了解下其实现,就一行代码^o^

但是,vague_func的模板类型推断规则,也造成了T类型的不确定(int还是int&?),这给后续的编码也带来了困难。

std::forward

在上述vague_func中,如果传入一个右值,但是val却是一个变量,也就是一个左值。那么如何保持原来实参的类型信息呢,这时需要用到std::forward。

std::forward(val)返回类型是T&&,这时,根据折叠规则,如果实参val是个左值,则返回T&;如果是右值,则返回T&&。

void f(int &&i)
{
    std::cout<<i<<"\t i is a right ref.\n";
}

void g(int &i)
{
    std::cout<<i<<"\t i is a left ref.\n";
}

template <typename F, typename T>
void forward_func(F f, T&& val)
{
    f(std::forward<T>(val));
}

std::cout<<"test forward:\n";
forward_func(f, 5);
forward_func(g, rref);
forward_func(g, val);
std::cout<<"test forward done.\n"<<std::endl;

所有程序的输出:

test move constructor:
old_strs[0]: abcde
new_strs[0]: abcde
old_strs[0]: 
test move constructor done.

test rvalue reference:
42      42      1764    1764    1764
test rvalue ref done.

test std::move:
bzdf    bzdf    bzdf
test std::move done.

test custom move copy constructor/move assign operator.
-------iv1:
capacity: 10
size: 5
pointer: 0x1523160
IntVect move constructor.
-------iv2:
capacity: 10
size: 5
pointer: 0x1523160
-------iv1:
capacity: 0
size: 0
pointer: nullptr
IntVect copy constructor.
-------iv3:
capacity: 10
size: 5
pointer: 0x1523190
-------iv2:
capacity: 10
size: 5
pointer: 0x1523160
-------iv4:
capacity: 5
size: 0
pointer: 0x15231c0
IntVect move assign constructor.
-------iv4:
capacity: 10
size: 5
pointer: 0x1523160
-------iv2:
capacity: 0
size: 0
pointer: nullptr
test custom move copy constructor/move assign operator done.

test move iterator:
new_strs[0]: 
new_strs2[0]: abcde
test move iterator done.

test ref folding:
-------with val:
val: 2
val2: 3 val: 2
-------with lref:
val: 2
val2: 3 val: 3
-------with rref:
val: 2
val2: 3 val: 3
val: 3
val2: 4 val: 3
test ref done.

test forward:
5        i is a right ref.
3        i is a left ref.
3        i is a left ref.
test forward done.

总结

  • 新标准中允许通过&&标识定义一个右值引用,将其绑定到一个右值上。
  • std::move函数的作用是获得一个变量的右值引用。
  • 移动构造,就是接受一个右值引用,从而接受(窃取)该右值引用所引用的对象,而没有实际的大块内存拷贝操作,并且保证被窃取后的对象可析构可销毁。
  • 可以定义自己的移动构造函数以及移动赋值运算。
  • noexcept用于向标准库指明此函数不会抛出异常。声明移动构造函数和移动赋值运算为noexcept以避免标准库在和我们定义的这个类进行交互时做一些不必要的工作。
  • 新标准中提供了std::make_move_iterator函数用于从普通迭代器获得移动迭代器。对移动迭代器解引用将会获得对应的右值引用,从而方便的对整个容器进行移动操作。
  • 引用折叠规则,除了T&& &&折叠为T&&之外的所有情况均折叠为T&,主要用于模板类型推断中。
  • std::forward(val)用于保持实参的左右值信息。

猜你喜欢

转载自blog.csdn.net/qq_23225317/article/details/79786829