无法完美转发的类型¶
用相同实参调用原函数和转发函数,如果两者执行不同的操作,则称完美转发失败。完美转发失败源于模板类型推断不符合预期,会导致这个问题的类型包括:大括号初始化值、作为空指针的 0 和 NULL、只声明但未定义的整型 static const 数据成员、重载函数的名称和函数模板名称、位域。
大括号初始化¶
void f(const std::vector<int>& v) {}
template<typename T>
void fwd(T&& x)
{
f(std::forward<T>(x));
}
f({ 1, 2, 3 }); //@ OK,{1, 2, 3}隐式转换为std::vector<int>
fwd({ 1, 2, 3 }); //@ 无法推断T,导致编译错误
//@ 解决方法是借用auto推断出std::initializer_list类型再转发
auto x = { 1, 2, 3 };
fwd(x); //@ OK
作为空指针的0或NULL¶
0 和 NULL 作为空指针传递给模板时,会推断为 int 而非指针类型:
void f(int*) {}
template<typename T>
void fwd(T&& x)
{
f(std::forward<T>(x));
}
fwd(NULL); //@ T推断为int,转发失败
只声明但未定义的static const整型数据成员¶
类内的 static 成员的声明不是定义,如果 static 成员声明为 const,则编译器会为这些成员值执行 const propagation,从而不需要为它们保留内存。对整型 static const 成员取址可以通过编译,但会导致链接期的错误。转发引用也是引用,在编译器生成的机器代码中,引用一般会被当成指针处理。程序的二进制代码中,从硬件角度看,指针和引用的本质相同。
class A {
public:
static const int n = 1; //@ 仅声明
};
void f(int) {}
template<typename T>
void fwd(T&& x)
{
f(std::forward<T>(x));
}
f(A::n); //@ OK:等价于f(1)
fwd(A::n); //@ 错误:fwd形参是转发引用,需要取址,无法链接
但并非所有编译器的实现都有此要求,上述代码可能可以链接。考虑到移植性,最好还是提供定义:
//@ A.h
class A {
public:
static const int n = 1;
};
//@ A.cpp
const int A::n;
重载函数的名称和函数模板名称¶
如果转发的是函数指针,可以直接将函数名作为参数,函数名会转换为函数指针:
void g(int) {}
void f(void(*pf)(int)) {}
template<typename T>
void fwd(T&& x)
{
f(std::forward<T>(x));
}
f(g); //@ OK
fwd(g); //@ OK
但如果要转发的函数名对应多个重载函数,则无法转发,因为模板无法从单独的函数名推断出函数类型:
void g(int) {}
void g(int, int) {}
void f(void(*)(int)) {}
template<typename T>
void fwd(T&& x)
{
f(std::forward<T>(x));
}
f(g); //@ OK
fwd(g); //@ 错误:不知道转发的是哪一个函数指针
转发函数模板名称也会出现同样的问题,因为函数模板可以看成一批重载函数:
template<typename T>
void g(T x)
{
std::cout << x;
}
void f(void(*pf)(int)) { pf(1); }
template<typename T>
void fwd(T&& x)
{
f(std::forward<T>(x));
}
f(g); //@ OK
fwd(g<int>); //@ 错误
要让转发函数接受重载函数名称或模板名称,只能手动指定需要转发的重载版本或模板实例。不过完美转发本来就是为了接受任何实参类型,而要传入的函数指针类型一般是未知的。
template<typename T>
void g(T x)
{
std::cout << x;
}
void f(void(*pf)(int)) { pf(1); }
template<typename T>
void fwd(T&& x)
{
f(std::forward<T>(x));
}
using PF = void(*)(int);
PF p = g;
fwd(p); //@ OK
fwd(static_cast<PF>(g)); //@ OK
位域¶
转发引用也是引用,实际上需要取址,但位域不允许直接取址:
struct A {
int a : 1;
int b : 1;
};
void f(int) {}
template<typename T>
void fwd(T&& x)
{
f(std::forward<T>(x));
}
A x{};
f(x.a); //@ OK
fwd(x.a); //@ 错误
实际上接受位域实参的函数也只能收到位域值的拷贝,因此不需要使用完美转发,换用传值或传 const 引用即可。完美转发中也可以通过强制转换解决此问题,虽然转换的结果是一个临时对象的拷贝而非原有对象,但位域本来就无法做到真正的完美转发。
fwd(static_cast<int>(x.a)); //@ OK