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

发布时间 2023-07-05 21:08:39作者: 冰山奇迹

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&&,保持参数右值属性