Back To Basics 笔记_01_移动语义与完美转发

发布于:2023-01-19 ⋅ 阅读:(132) ⋅ 点赞:(0)

Move Semantics

关于移动语义与完美转发,Back To Basics 2021 讲解的深入浅出,因此做此篇笔记进行记录


首先看下面三行代码


string getData() {
    return "data\0";
}
int main()
{
    std::vector<std::string> coll;
    coll.reserve(3);

    std::string s{ getData() };

    coll.push_back(s);

	coll.push_back(getData());
}

经过这几行代码,在C++ 98 堆栈中的情况如下:

可以看到,代码中先是将getData中的返回值复制给了s,因此s占用了一段堆空间,之后执行

coll.push_back(s);时是进行了一次堆复制,给到coll的第一段空间

而coll.push_back(getData());在执行时:

getData() 在return的时候,会申请一段堆空间,之后将这段堆空间中的内容复制给coll的第二段,这种方式当然是没有bug出现的,但可以明显的看到,getData所申请的空间利用率较低,申请堆空间再进行释放也无疑会占用资源。

而C++11中引入了新特性来解决这种资源浪费,即移动语义:

 将栈中的getData返回地址删除,直接将堆空间中的内存交给coll

毫无疑问,这种方式是比用一个字符串s进行复制来的更快,但可读性也会因此降低,代码的可读性也是开发时候的重点。

因此C++11中引入了另一种新特性std::move();

如果将上面的代码更改,用这种方式执行:

string getData() {
    return "data\0";
}


int main()
{
    std::vector<std::string> coll;
    coll.reserve(3);

    std::string s{ getData() };

    coll.push_back(s);

    coll.push_back(std::move(s));
}

堆栈中的情况则如下:

 s则变为了将亡值,即有效但未指明的状态。

当然此时就可以再次对s进行赋值变为正常值。

比较推荐的用法为如下:

1、输入时进行内存转移

std::vector<std::string> allRows;
std::string row;
while (std::getline(myStream, row)) {
    allRows.push_back(std::move(row));
}

2、交换两个变量:

void swap(std::string& a, std::string& b) {
    std::string tmp{std::move(a)};
    a = std::move(b);
    b = std::move(tmp);
}

C++的vector中内置了两种push_back:一种带移动语义一种不带,可以根据在vs中直接跳转到 两个不同的实现:

这里可以直接看到C++对coll.push_back(getData());进行了优化直接使用了移动语义的版本,即使用右值引用来将getData() 返回地址进行转交。

移动语义在类中的使用

如果类中不声明移动构造函数和拷贝构造函数,那么移动构造函数是默认存在的,请看下面的代码:


class Cust {
private:

    std::string first;
    std::string last;
    int         val;
public:

    Cust(const std::string& f, const std::string& l, int v) : first{ f }, last{ l }, val{ v } {}
    
    friend std::ostream& operator<< (std::ostream& strm, const Cust& c) {
        return strm << "[" << c.val << ":" << c.first << " " << c.last << "]";
    }
};

int main()
{

    std::vector<Cust> v;
    Cust c2{ "Joe","Fox",77 };
    v.push_back(std::move(c2));
}

如果传入的是一个右值,则在执行的时候回默认直接调用移动构造函数

但如果声明了一段拷贝构造函数,如下:


class Cust {
private:

    std::string first;
    std::string last;
    int         val;
public:

    Cust(const std::string& f, const std::string& l, int v) : first{ f }, last{ l }, val{ v } {}
    Cust(const Cust&) = default;// 拷贝构造
    friend std::ostream& operator<< (std::ostream& strm, const Cust& c) {
        return strm << "[" << c.val << ":" << c.first << " " << c.last << "]";
    }
};

int main()
{

    std::vector<Cust> v;
    Cust c2{ "Joe","Fox",77 };
    v.push_back(std::move(c2));
}

则在执行的时候即使使用了右值进行传值,也不会将所有权转交,而是自动调用拷贝构造

 可以看到c2中仍然保存着原有的内容,没有变为将亡值。

如果自行已了移动构造函数:


class Cust {
private:

    std::string first;
    std::string last;
    int         val;
public:

    Cust(const std::string& f, const std::string& l, int v) : first{ f }, last{ l }, val{ v } {}
    // Cust(const Cust&) = default;// 拷贝构造
    Cust(const Cust& c): first{c.first},last{c.last},val{c.val}{}// 拷贝构造
    Cust(Cust &&c) // 移动构造
        : first{ std::move(c.first) }, last{ std::move(c.last) }, val{ c.val }{
        c.val *= -1;
    }
    friend std::ostream& operator<< (std::ostream& strm, const Cust& c) {
        return strm << "[" << c.val << ":" << c.first << " " << c.last << "]";
    }
};

执行的结果如下:

 因为代码中转交val的所有权而是将其*-1 因此还存在内容 -77。

完美转发:

如果我们想要一段代码能够接受所有的值类型,则需要对一段代码进行三次重载:

而如果我们一个函数有三个形参,那么我们则需要27个重载来实现这种功能。

但这种形式显然很麻烦,因此C++11引入了另一种新特性,完美转发

完美转发需要有三个要点:

1、使用模板变量

2、定义变量的时候带上双引用符号&&

3、使用std::forward<>() 进行所有权的转交

即使用如下方式定义:

class C {

};

void foo(const C&);
void foo(C&);
void foo(C&&);

template<typename T>
void callFoo(T&& x) {
    foo(std::forward<T>(x));
}

这样则可以接受任何类型的参数,只有在x为右值时,std::forward<>() 才退化为std::move()

右值引用和universal reference(万能引用或未定义引用)的区别

如下只有在使用模板进行定义的时候,才代表universal reference 其他则都是右值引用

而vector中如何对此进行支持的呢?

引入了emplace_back:情况emplace_back的源码:

template <class... _Valty>
    _CONSTEXPR20_CONTAINER decltype(auto) emplace_back(_Valty&&... _Val) {
        // insert by perfectly forwarding into element at end, provide strong guarantee
        auto& _My_data   = _Mypair._Myval2;
        pointer& _Mylast = _My_data._Mylast;
        if (_Mylast != _My_data._Myend) {
            return _Emplace_back_with_unused_capacity(_STD forward<_Valty>(_Val)...);
        }

emplace_back 中使用的就是univesal reference

此时我们则可以调用emplace_back 来进行之前我们预想的那种复杂入口参数:


class Cust {
private:

    std::string first;
    std::string last;
    int         val;
public:

    Cust(const std::string& f, const std::string& l, int v) : first{ f }, last{ l }, val{ v } {}
    // Cust(const Cust&) = default;// 拷贝构造
    Cust(const Cust& c): first{c.first},last{c.last},val{c.val}{}// 拷贝构造
    Cust(Cust &&c) // 移动构造
        : first{ std::move(c.first) }, last{ std::move(c.last) }, val{ c.val }{
        c.val *= -1;
    }
    friend std::ostream& operator<< (std::ostream& strm, const Cust& c) {
        return strm << "[" << c.val << ":" << c.first << " " << c.last << "]";
    }
};
int main(){


    std::vector<Cust> v;
    Cust c2{ "Joe","Fox",77 };
    v.push_back(std::move(c2));
    std::cout << "c2:" << c2 << endl;
    
	std::string tempfirst{ "Jil" };
	std::string templast{ "Cook" };

    v.emplace_back(std::move(templast), templast, 39);
}

而不是在vector中实现27中形式的push_back.

显然这种混合了右值,左值的情况,push_back是无法实现的:

如果调用则会报错

 auto&& 作为右值/未定义 引用的各种情况:

可以看到auto& 是不能接受右值,而auto&&则可以。


网站公告

今日签到

点亮在社区的每一天
去签到