C++ 完美转发机制

参数转发(Parameter Forwarding)是指在 C++ 中将函数参数原封不动地传递给另一个函数的技术。完美转发 (Perfect Forwarding) 则是为了在参数传递过程中避免不必要的拷贝或移动,从而保持参数的原始类型和属性。

  • 保持参数类型和属性:在函数模板中,有时我们希望传递的参数能够保留其左值(lvalue)或右值(rvalue)的属性。这是因为左值引用和右值引用的行为和性能不同,特别是在涉及资源管理时(如内存、文件句柄等),直接影响程序的效率和正确性。
  • 避免不必要的拷贝和移动:如果不能正确区分参数的左值和右值属性,在函数内部处理这些参数时可能会导致不必要的拷贝或移动操作,这会带来额外的开销和潜在的性能问题。

1. 问题场景

在 C++ 中,有很多 make 开头的工厂函数。我们也编写一个工厂函数,从中去发现一些问题。

#include <iostream>
using namespace std;


struct Person
{
	Person(string&) 
	{ 
		cout << "左值" << endl; 
	}

	Person(string&&)
	{ 
		cout << "右值" << endl; 
	}
};


// 问题:
// 1. param 参数会导致对象发生拷贝、移动操作
// 2. param 传递到构造函数,无法区分值的类别(左值、右值)
Person make_person(string param)
{
	// 如果传递的参数 param 是左值
	return Person(param);
}


void test()
{
	// 1. 传递左值
	string name = "Obama";
	make_person(name);

	// 2. 传递右值
	make_person(move(name));
}


int main()
{
	test();
	return 0;
}

程序执行结果:

左值
左值

2. 引用折叠

#include <iostream>
#include <type_traits>
using namespace std;


struct Person
{
	Person(string&)
	{
		cout << "左值" << endl;
	}

	Person(string&&)
	{
		cout << "右值" << endl;
	}
};


// 思考过程:
// 为了避免拷贝,需要使用引用参数
// 如果写左值引用,则无法匹配右值对象
// 如果写右值引用,则无法匹配左值对象
// 如果写常量引用,则无法匹配 Person 构造函数
// 所以,通过模板推导类型
// 写 T&,就无法匹配右值
// 写 const T& 会给传递进来的参数增加 const 性,也不合理
// 只能写 T&&
// 写 T&& 可以匹配右值,为什么可以匹配左值?
// 这就需要了解 C++11 中的引用折叠

// 函数要能够实现无拷贝,就需要实现对左值和右值的引用
template<class T>
Person make_person(T&& param)
{
	cout << "是否左值:" << is_lvalue_reference<T&&>::value << endl;
	cout << "是否右值:" << is_rvalue_reference<T&&>::value << endl;
	// 如果传递的参数 param 是左值
	return Person(param);
}


void test()
{
	// 1. 传递左值
	string name = "Obama";
	make_person(name);

	cout << "---------" << endl;

	// 2. 传递右值
	make_person(move(name));
}


int main()
{
	test();
	return 0;
}
是否左值:1
是否右值:0
左值
---------
是否左值:0
是否右值:1
左值

引用折叠是 C++11 引入的一个特性,它指的是当多种引用类型(左值引用或右值引用)组合在一起时,通过一组规则确定最终的引用类型。

  • 如果任何一个引用是左值引用,则结果为左值引用。
  • 否则(即所有引用都是右值引用),则结果为右值引用。
template<class T>
Person make_person(T&& param)
{
	// 如果传递的参数 param 是左值
	return Person(param);
}


void test()
{
	// 1. 传递左值
	string name = "Obama";
	make_person(name);

	// 2. 传递右值
	make_person(move(name));
}
make_person(模板函数参数)T 类型(传递参数类型)最终类型
T&T&T&
T&&T&T&
T&&T&&T&&
  • 如果 make_person 的参数设置为 T&
    • 如果传递左值,则 make_person 的 T& 最终将被确定为左值;
  • 如果 make_person 的参数设置为 T&&
    • 如果传递左值,则 make_person 的 T&& 最终将被确定为左值;
    • 如果传递右值,则 make_person 的 T&& 最终将被确定为右值;

3. 完美转发

在前面的实现中,我们可以做到 make_person 在不拷贝的情况下,接收到外部传递的左值和右值参数。但是,再次将值转发到 Person 构造函数时,发现值的类型、值的性质(左值、右值)丢失,使得无法正确调用 Person 的构造函数。

#include <iostream>
#include <type_traits>
using namespace std;


struct Person
{
    Person(string&)
    {
        cout << "左值" << endl;
    }

    Person(string&&)
    {
        cout << "右值" << endl;
    }
};


// 问题:
// 1. param 参数会导致对象发生拷贝、移动操作
// 2. param 传递到构造函数,无法区分值的类别(左值、右值)

// 如何避免拷贝和移动?
// 使用引用参数
// 左值引用:就无法接收右值参数
// 右值引用:就无法接收左值参数
// 常量引用:万能引用,既可以引用左值、也可以引用右值
// 原因:由于我们写的是常量引用,这样使得传递的参数增加 const 性
// 希望参数“原封不动”传递到目标函数中

// C++ 中支持泛型(模板技术),函数模板而言,很重要的特性,可以实现类型的自动推导
// T
// T&  只能匹配左值
// T&& 可以了
// const T& 不能写,会给参数增加额外的 const 性
template<class T>
Person make_person(T&& param)
{
    // 疑问:由于写的是 T&& 看起来是一个右值引用,是不是说,无论传递进来的是左值还是右值,都被转换为右值类型?
    // 看看,参数究竟能否正确区分左值和右值
   
    cout << "是否左值:" << is_lvalue_reference<T&&>::value << endl;
    cout << "是否右值:" << is_rvalue_reference<T&&>::value << endl;

    // T&& 通过引折叠能够确定最终的类型:左值引用、右值引用
    // 左值引用:理解为对左值对象的一个别名
    // 右值引用:右值是即将被废弃的对象,右值引用目的就是为了给这些即将废弃的对象续命
    // 此时,无论左值对象、右值对象都变成具名对象(有名字的对象),是一个左值对象了
    // 怎么解决?
    // 将 param 再转换回原来的类型,传递到 Person 构造函数里就可以了
    // return Person((T&&)param);
    // 支持,实现C++参数的完美转发

    // std::forward();
    // return Person(static_cast<T&&>(param));
   
    // 建议大家使用 forward 函数
    return Person(forward<T>(param));
}


void test()
{
    // 1. 传递左值
    string name = "Obama";
    make_person(name);
    
    // 2. 传递右值
    make_person(move(name));
}


int main()
{
    test();
    return 0;
}


2024/06/10更新

最后完美转发一定要强转成T&&吗,强转成T会有什么缺陷吗?我实测强转成T传入构造函数,也能达到一样的效果

如果写 T 的话,类型转化会发生拷贝行为。

未经允许不得转载:一亩三分地 » C++ 完美转发机制
评论 (1)

7 + 9 =

  1. avatar
    AerithSu06-06 8:34回复

    很有帮助👍