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;
}

引用折叠是 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;
}

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部