C++ 逆向之 forward 函数与完美转发

在进行 std::forward 函数的讲解之前,需要知道 std::move 的运行原理,还不是很清楚的朋友建议先看一下前置知识,本次内容是基于 std::move 内容的基础上进行讲解:
C++ 逆向之 move 函数

然后来讲解我们今天的主角:std::forward 函数与完美转发

一、std::forward 函数的作用

std::forward 函数是 C++ 标准库中的一个模板函数,它的主要作用有两个:

  1. 用于在模板代码中转发参数,保持参数的左值或右值语义不变
  2. 这个函数是完美转发(perfect forwarding)的关键部分,它允许函数模板接受任意类型的参数,并将这些参数转发给其他函数,同时保持参数原有的值类别(左值或右值)

光看定义可能感受并不深刻,我们直接上例子,先来看一下不用完美转发的情况:

class MyClass
{
public:
	// 显式的删除无参构造函数
	MyClass() = delete;

	// 有参构造函数
	MyClass(int value) : value(value)
	{
		std::cout << "调用了有参构造函数:MyClass(int value)" << std::endl;
	}

	// 拷贝构造函数
	MyClass(const MyClass& other) : value(other.value)
	{
		std::cout << "调用了拷贝构造函数:MyClass(MyClass& other)" << std::endl;
	}

	// 移动构造函数
	MyClass(MyClass&& other) noexcept : value(other.value)
	{
		std::cout << "调用了移动构造函数:MyClass(MyClass&& other)" << std::endl;
		other.value = 0;
	}

	// 析构函数
	~MyClass()
	{
		std::cout << "调用了析构函数:~MyClass()" << std::endl;
	}

private:
	int value;
};

template <typename T>
MyClass* Creator(T&& other)
{
	return new MyClass(other);
}

int main()
{
	MyClass myClass1(10);
	MyClass* myClass2 = Creator(myClass1);
	MyClass* myClass3 = Creator(std::move(myClass1));

	delete myClass2;
	delete myClass3;
	
	return 0;
}

在上面的代码中,我们构造了一个类,为这个类分别配备了有参构造函数、拷贝构造函数和移动构造函数,我们知道拷贝构造函数是会发生内存拷贝的,对于大型的对象来说会有很大的性能开销,而移动构造函数不会发生值拷贝现象。

此外,我们还添加了一个函数模板,用于传入一个类,并重新构造一个新的类进行返回。

在主函数中,我们首先利用有参构造函数实例化了一个对象 myClass1,接下来我们将 myClass1 作为左值传入了模板,然后调用拷贝构造函数,最后,我们将 myClass1 通过 std::move 函数转化为右值作为参数传递给模板,我们想要这个模板调用移动构造函数,避免性能上的开销,实际上结果怎么样呢?如下:

我们看到,虽然我们传入了一个右值,但是还是调用了拷贝构造函数,这是为什么呢?我们在分析 std::move 函数的时候说过,myClass1 会被当做左值传递,然后我们可以利用 std::move 将其转化为右值进行参数传递,那么在函数里面 myClass 就是右值,但是这里为什么会当做左值并调用拷贝构造函数呢?

其实我们之前分析的没问题,在这个例子中,myClass1 被作为右值参数传进去的时候,在模板函数体里面还是右值,但是!模板函数里面还调用了构造函数return new MyClass(other);,相当于这个右值又被当参数传给了一个新的函数体,那么这个右值会被强制转为左值,传递给return new MyClass(other);,所以最终调用了拷贝构造函数,造成了性能上的开销。

二、完美转发

那么有没有方法避免上面的情况呢?我们想要的结果是传进去的是左值,不管在模板里面嵌套多少个函数体,一直保持左值这个语义不丢失,右值同理,那么我们就需要用到 std::forward 函数,构造一个完美转发,这个函数就是为了解决这个问题而诞生的,我们将模板函数进行修改:

template <typename T>
MyClass* Creator(T&& other)
{
	return new MyClass(std::forward<T>(other));
}

我们在模板最外层的函数体里面,参数还是右值语义没有丢失,如果想要继续保持右值语义,那么我们就需要在传入模板里面嵌套函数的时候,通过 std::forward 函数进行转发,那么在嵌套的子函数里面仍然会保持右值语义,左值同理。

我们来看在通过 std::forward 构造了完美转发后的运行的结果:

这个时候我们就实现了,在子函数里面,myClass1 仍然被当做右值,并且调用了移动构造函数,不会产生额外的性能开销,这就是std::forward完美转发 存在的意义!

三、逆向 std::forward 函数

那么在了解完用法后,感兴趣的朋友可以继续跟我来看看底层的实现原理,其实和 std::move 大同小异,我们先扒出vs 中 std::forward 的底层代码:

template <class _Ty>
struct remove_reference {
	using type = _Ty;    // 如果参数是非引用类型,则直接返回非引用类型本身 _Ty
	using _Const_thru_ref_type = const _Ty;
};

template <class _Ty>
struct remove_reference<_Ty&> {
	using type = _Ty;    // 如果参数是左值引用,则移除左值引用,返回其底层的非引用类型(参数为 int& 则返回 int)
	using _Const_thru_ref_type = const _Ty&;
};

template <class _Ty>
struct remove_reference<_Ty&&> {
	using type = _Ty;    // 如果参数是右值引用,则移除右值引用,返回其底层的非引用类型(参数为 int&& 也返回 int)
	using _Const_thru_ref_type = const _Ty&&;
};

template <class _Ty>
using remove_reference_t = typename remove_reference<_Ty>::type;    // 移除左值或右值引用,返回其底层的非引用类型

template <class _Ty>
_NODISCARD constexpr _Ty&& forward(
    remove_reference_t<_Ty>& _Arg) noexcept { // forward an lvalue as either an lvalue or an rvalue
    return static_cast<_Ty&&>(_Arg);
}

template <class _Ty>
_NODISCARD constexpr _Ty&& forward(remove_reference_t<_Ty>&& _Arg) noexcept { // forward an rvalue as an rvalue
    static_assert(!is_lvalue_reference_v<_Ty>, "bad forward call");
    return static_cast<_Ty&&>(_Arg);

template <class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) noexcept { // forward _Arg as movable
	return static_cast<remove_reference_t<_Ty>&&>(_Arg);
}

关于 remove_reference 这里不再赘述,且下面的分析会涉及引用折叠,不懂的朋友请看 std::move 那一节,里面有详细的介绍。

我们看 std::forwardstd::move 的定义非常的像,不同的地方是 std::forward 有两个特例版本,一个接收左值引用,一个接受右值引用,而 std::move 用的是一个万能引用 _Ty&&

我们来看:

  1. 当我们模板里面传入一个左值或左值引用的时候,_Ty=MyClass&,那么就会调用 std::forward 的第一个特例版本并返回:return static_cast<_Ty&&>(_Arg); => return static_cast<MyClass&&&>(_Arg); => 发生引用折叠:return static_cast<MyClass&>(_Arg);,最终返回一个左值引用。

  2. 当我们模板里面传入一个右值引用的时候,_Ty=MyClass&&,那么就会调用 std::forward 的第二个特例版本并返回:return static_cast<_Ty&&>(_Arg); => return static_cast<MyClass&&&&>(_Arg); => 发生引用折叠:return static_cast<MyClass&&>(_Arg);,最终返回一个右值引用。

因此,std::forward 函数通过这种方式,可以在模板中进行参数传递的时候保持左值或右值的语义不发生变化,从而实现完美转发的功能。

posted @ 2024-11-06 12:16  lostin9772  阅读(17)  评论(0编辑  收藏  举报