首页 > 其他分享 >性能优化利器 std::move/forward 实现原理

性能优化利器 std::move/forward 实现原理

时间:2023-07-05 21:11:13浏览次数:64  
标签:std __ string move Test && forward

utility 包含了 STL 经常使用的几个模板函数的定义:std::move() 用于得到一个右值引用;std::swap() 使用移动语义,交换两个对象;std::forward() 支持完美转发。本文分析了上述三个模板函数的实现原理。

本文内容:

  • 1、std::move

  • 2、std::swap

  • 3、std::forward

 

1、std::move

std::move() 函数获得一个右值引用

/// since C++11, until C++14
template< class T >
typename std::remove_reference<T>::type&& move( T&& t ) noexcept;

/// since C++14
template< class T >
constexpr std::remove_reference_t<T>&& move( T&& t ) noexcept;

右值引用可以触发移动语义,使用资源交换而不是拷贝的方法完成赋值。例如下面的示例,(2)触发移动语义,调用完成,str 成为了空字符串,因为进行了资源交换。

#include <iostream>
#include <string>
#include <utility>
#include <vector>

int main() {
std::string str = "Salut";
std::vector<std::string> v;

v.push_back(str); // (1)
std::cout << "After copy, str is " << str << '\n';

v.push_back(std::move(str)); // (2)
std::cout << "After move, str is " << str << '\n';

std::cout << "The contents of the vector are { ";
for (auto& elm : v) {
std::cout << elm << ' ';
}
std::cout << "}\n";
return 0;
}

执行的输出为

After copy, str is Salut
After move, str is
The contents of the vector are { Salut Salut }

std::move() 的实现非常简单

/// include/bits/move.h
template<typename _Tp>
_GLIBCXX_NODISCARD
constexpr typename std::remove_reference<_Tp>::type&&
move(_Tp&& __t) noexcept
{ return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

理解 std::move() 需要明白如下两点

(1)引用折叠

  • 间接创建(只能间接,如类型别名或模板参数,语法不支持直接创建)引用的引用,这些引用会形成“折叠”

  • T& &、T& &&、T&& & 都会折叠成左值引用 T&

  • T&& && 折叠成右值引用 T&&

(2)模板右值引用参数

  • 模板函数形参类型是 T&&,而实参是一个左值,推断出的模板实参类型将是 T&,且函数参数将被实例化一个普通的左值引用参数 T&

  • 模板函数形参类型是 T&&,而实参是一个右值,推断出的模板实参类型将是 T,且函数参数将被实例化一个普通参数 T

(3)从一个左值 static_cast 到一个右值引用是允许的

明白上述(1)(2)(3)三点就可以理解 std::move() 的原理了。

如果 std::move() 实参是右值,如下所示:

auto s2 = std::move(std::string("bye!"));

模板函数 move 函数将会被实例化为如下类型

std::string&& move(std::string&& t);
  • 模板参数 Tp 被推断为 std::string 类型

  • 形参类型右值引用 std::string&&

  • std::remove_reference 用 std::string 实例化

  • std::remove_reference<std::string> 的 type 成员是 std::string

  • 返回类型是 std::string&&

如果 std::move() 实参是左值,如下所示:

std::string s1 = std::string("bye"); 
auto s2 = std::move(s1);

模板函数 move 将会被实例化为

std::string&& move(std::string& t);
  • 模板参数 Tp 被推断为 std::string& 类型

  • 形参类型是 std::string& &&,引用折叠为左值引用 std::string&

  • std::remove_reference 用 std::string& 实例化

  • std::remove_reference<std::string&> 的 type 成员是 std::string

  • 返回类型是 std::string&&

 

2、std::swap()

通过移动类交换两个变量。它要求传入的变量是可以移动的(具有移动构造函数和重载移动赋值运算符)。宏定义 _GLIBCXX_MOVE 在 C++11 展开为 std::move()。

/// include/bits/move.h
template<typename _Tp>
_GLIBCXX20_CONSTEXPR
inline
#if __cplusplus >= 201103L
typename enable_if<__and_<__not_<__is_tuple_like<_Tp>>,
is_move_constructible<_Tp>,
is_move_assignable<_Tp>>::value>::type
#else
void
#endif
swap(_Tp& __a, _Tp& __b)
_GLIBCXX_NOEXCEPT_IF(__and_<is_nothrow_move_constructible<_Tp>,
is_nothrow_move_assignable<_Tp>>::value)
{
#if __cplusplus < 201103L
///...
#endif
_Tp __tmp = _GLIBCXX_MOVE(__a);
__a = _GLIBCXX_MOVE(__b);
__b = _GLIBCXX_MOVE(__tmp);
}

template<typename _Tp, size_t _Nm>
_GLIBCXX20_CONSTEXPR
inline
#if __cplusplus >= 201103L
typename enable_if<__is_swappable<_Tp>::value>::type
#else
void
#endif
swap(_Tp (&__a)[_Nm], _Tp (&__b)[_Nm])
_GLIBCXX_NOEXCEPT_IF(__is_nothrow_swappable<_Tp>::value)
{
for (size_t __n = 0; __n < _Nm; ++__n)
swap(__a[__n], __b[__n]);
}

在移动的过程中,不会有资源的申请和释放。看下面的例子:

#define ADD_MOVE

class Test {
public:
explicit Test(size_t l): len_(l), data_(new unsigned char[l]) {
printf("Test Ctor\n");
}
Test(const Test& other): len_(other.len_), data_(new unsigned char[len_]) {
printf("Test Copy Ctor\n");
memmove(data_, other.data_, len_);
}
Test& operator=(const Test& rhs) {
printf("Test operator=\n");
if(this != &rhs) {
len_ = rhs.len_;
data_ = new unsigned char[len_];
memmove(data_, rhs.data_, len_);
return *this;
}
}
#ifdef ADD_MOVE
Test(Test&& other): len_(len_), data_(other.data_) {
printf("Test Move Ctor\n");
other.len_ = 0;
other.data_ = nullptr;
}
Test& operator=(Test&& rhs) {
printf("Test Move operator=\n");
if(this != &rhs) {
len_ = rhs.len_;
data_ = rhs.data_;
rhs.len_ = 0;
rhs.data_ = nullptr;
}
}
#endif
~Test() {
printf("Test Dtor\n");
delete[] data_;
}

private:
size_t len_;
unsigned char* data_;
};

我们重载 operator new() 和 operator delete() 输出分配或释放内存操作

void* operator new(size_t size) {
printf("allocate memory\n");
return malloc(size);
}

void operator delete(void* ptr) {
printf("free memory\n");
free(ptr);
}

进行交换测试

int main() {
Test a(1), b(1);
printf("===========Swap==========\n");
std::swap(a, b);
printf("===========Done==========\n");
return 0;
}

在对象“无法”移送的时候(没有显式移动构造函数和移动赋值运算符),交换需要拷贝资源

+Allocate Memory
Test Ctor
+Allocate Memory
Test Ctor
===========Swap==========
+Allocate Memory
Test Copy Ctor
Test operator=
+Allocate Memory
Test operator=
+Allocate Memory
Test Dtor
-Free Memory
===========Done==========
Test Dtor
-Free Memory
Test Dtor
-Free Memory

当对象可以移动时,交换没有资源的拷贝

+Allocate Memory
Test Ctor
+Allocate Memory
Test Ctor
===========Swap==========
Test Move Ctor
Test Move operator=
Test Move operator=
Test Dtor
===========Done==========
Test Dtor
-Free Memory
Test Dtor

-Free Memory

 

3、std::forward()

std::forward() 是为了支持 C++11 转发。

某些函数需要将其一个或多个实参连同类型不变地转发给其他函数,在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是 const 的以及实参是左值还是右值。

比如如下例子,Data 构造函数可以接受参数类型是 int&&, int&,模板函数 MakeData 可以接受左值和右值(和 std::move() 相同的原理)

#include <iostream>
#include <memory>
#include <utility>

struct Data {
Data(int&& n) { std::cout << "rvalue overload, n=" << n << '\n'; }
Data(int& n) { std::cout << "lvalue overload, n=" << n << '\n'; }
};

template <typename Tp>
std::unique_ptr<Data> MakeData(Tp&& value) {
return std::unique_ptr<Data>(new Data(value));
}

int main() {
auto data1 = MakeData(1); // rvalue
int n = 2;
auto data2 = MakeData(n); // lvalue

return 0;
}

但是无论传递给 MakeData() 的是左值还是右值,都是调用参数类型为 int& 的构造函数。

lvalue overload, n=1
lvalue overload, n=2

如果使用 std::forward() 函数,则可以将 value 的类型完全转发。

#include <iostream>
#include <memory>
#include <utility>

struct Data {
Data(int&& n) { std::cout << "rvalue overload, n=" << n << '\n'; }
Data(int& n) { std::cout << "lvalue overload, n=" << n << '\n'; }
};

template <typename Tp>
std::unique_ptr<Data> MakeData(Tp&& value) {
return std::unique_ptr<Data>(new Data(std::forward<Tp>(value)));
}

int main() {
auto data1 = MakeData(1);
int n = 2;
auto data2 = MakeData(n);

return 0;
}

传入右值可以调用到参数类型为 int&& 的构造函数。

rvalue overload, n=1
lvalue overload, n=2

需要注意的是,std::forward() 必须通过显式模板实参来调用。std::forward() 返回该显式实参类型的右值引用。通过其返回类型上的引用折叠,std::forward() 可以保持给定实参的左值/右值属性。

/// include/bits/move.h
template<typename _Tp>
_GLIBCXX_NODISCARD
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }

template<typename _Tp>
_GLIBCXX_NODISCARD
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
{
static_assert(!std::is_lvalue_reference<_Tp>::value,
"std::forward must not be used to convert an rvalue to an lvalue");
return static_cast<_Tp&&>(__t);
}

std::forward() 的需要结合右值引用模板函数,才能实现完美转发。如果 MakeData() 函数一样

template <typename Tp>
std::unique_ptr<Data> MakeData(Tp&& value) {
return std::unique_ptr<Data>(new Data(std::forward<Tp>(value)));
}
  • 如果 MakeData() 函数实参是左值,模板参数被推断为 Tp&,std::forward() 的返回值为 Tp& &&,引用折叠后是 Tp&,保持参数左值属性

  • 如果 MakeData() 函数实参是右值,模板参数被推断为 Tp,std::forward() 的返回值为 Tp&&,保持参数右值属性

标签:std,__,string,move,Test,&&,forward
From: https://www.cnblogs.com/blizzard8204/p/17529803.html

相关文章

  • C++面试八股文:std::array如何实现编译器排序?
    C++面试八股文:std::array如何实现编译器排序?某日二师兄参加XXX科技公司的C++工程师开发岗位第25面:面试官:array熟悉吗?二师兄:你说的是原生数组还是std::array?面试官:你觉得两者有什么区别?二师兄:区别不是很大,原生数组(非动态数组)和std::array都在栈上开辟空间,初始化的时候......
  • Mac中VscodeC++万能头文件配置bits/stdc++.h
    /Library/Developer/CommandLineTools/usr/include路径下创建bits文件夹新建stdc++.h头文件,内容如下//C++includesusedforprecompiling-*-C++-*-//Copyright(C)2003-2018FreeSoftwareFoundation,Inc.////ThisfileispartoftheGNUISOC++Library.T......
  • C++面试八股文:std::vector和std::list,如何选择?
    C++面试八股文:std::vector和std::list,如何选择?某日二师兄参加XXX科技公司的C++工程师开发岗位第24面:面试官:list用过吗?二师兄:嗯,用过。面试官:请讲一下list的实现原理。二师兄:std::list被称为双向链表,和C中手写双向链表本质上没有大的区别。list对象中有两个指针,一个指向......
  • 分布式文件存储 - FastDFS 工具类
    一、FastDFSClientpackagecom.changgou.file.util;importorg.csource.common.NameValuePair;importorg.csource.fastdfs.*;importorg.slf4j.LoggerFactory;importorg.springframework.core.io.ClassPathResource;importjava.io.ByteArrayInputStream;importjav......
  • C++面试八股文:std::vector了解吗?
    某日二师兄参加XXX科技公司的C++工程师开发岗位第23面:面试官:vector了解吗?二师兄:嗯,用过。面试官:那你知道vector底层是如何实现的吗?二师兄:vector底层使用动态数组来存储元素对象,同时使用size和capacity记录当前元素的数量和当前动态数组的容量。如果持续的push_back(empl......
  • STD-study-暑假-大一下-PTA-day1
    L1-001#include<iostream>usingnamespacestd;intmain(){cout<<"HelloWorld!"<<endl;return0;}毫无难度L1-002打印沙漏#include<stdio.h>#include<math.h>intmain(){intn;//符号的个数charc;//符号......
  • C++面试八股文:知道std::unordered_set/std::unordered_map吗?
    某日二师兄参加XXX科技公司的C++工程师开发岗位第27面:面试官:知道std::unordered_set/std::unordered_map吗?二师兄:知道。两者都是C++11引入的新容器,和std::set和std::map功能类似,key唯一,unordered_map的value可变。二师兄:不同于set/map,unordered_set/unordered_map都是无序容器......
  • std::quoted试用学习
    std::quoted是干啥用的,有啥作用?看是c++14和17中加入的。quoted这个单词似乎在计算机里面就有着特殊的意思,可惜没记住。英文原版资料看的少。  在cppreference网站中的示例如下:(https://en.cppreference.com/w/cpp/io/manip/quoted)voiddefault_delimiter(){conststd......
  • list + forward_list
       ......
  • stdlib.h
      ......