完美转发失败情况

完美转发不仅要转发对象,还包括其特征,型别特征、左值还是右值,const或volation饰词等。完美转发必须利用通用引用,只有通用引用形参才会将实参的左值还是右值信息编码。 所以完美转发不只是要有通用引用函数模板还要有目标函数,既然能够接受任意参数类型(不太严谨)那么也不能限制参数个数,以下面例子为说明:

template<typename... Ts>
void fwd(Ts&&... params)
{
	TargetFun(std::forward<Ts>(params)...);
}

完美转发的构成元素可以总结有以下几点:

  1. 通用引用函数模板
  2. 变长形参包
  3. 目标函数
  4. 运用std::forward

但是这里要注意有时同样的实参调用函数TargetFun可以编译通过,但是以同样的实参调用fwd确不能通过编译,当遇到以下情况恰恰说明了这点

大括号初始物


例如我们的目标函数:

void TargetFun(const std::vector<int>& v)
{
	
}

有如下调用:

// 可以通过编译
TargetFun({1,2,3});

// 无法通过编译
fwd({1,2,3});

对于一个非模板函数的调用在编译阶段通常要比较实参型别和形参型别是否兼容,然后如果有必要会实行隐式型别转换来使得调用成功

  • TargetFun({1,2,3})

    有了上面这个事实就可以解释本例子编译通过的原因,对于调用TargetFun({1,2,3}),编译器会以{1,2,3}生成一个std::vector型别对象,函数TargetFun形参就有了可以绑定对象

  • fwd({1,2,3})

    这个调用通过通用引用函数模板,失败的原因是向未声明为std::initializer_list型别的函数模板形参传递了大括号初始化物,这个理由着实按照C++标准执行,C++标准称为非推导语境, 那么出现了非推导语境编译器就会拒绝调用,在这篇文章提到auto变量在以大括号初始化物初始化时,那么这个变量推导为std::initializer_list,所以针对这个调用失败的 情况可以先声明之,就可以解决,例如:

      auto il = {1,2,3};
      fwd(il)
    

0和NULL作为空指针


在这篇文章介绍了0、NULL、nullptr的区别,当0和NULL传递给模板时推导结果通常是int而非所传递实参指针型别,所以0和NULL不能作为空指针进行完美转发,取而代之的是nullptr

仅声明的整型static const 成员变量


C++有个关于static const 成员变量的规定:这类变量只需声明,不需要给出类中整型static const 成员变量的定义。编译器通常会根据这些变量的值实施常数传递,而就不必再为它们保留内存。 例如下面的例子:

class CTest
{
public:
	static const std::size_t minVal = 28;
};

void TargetFun(const std::size_t v)
{
}
{
	TargetFun(CTest::minVal);
	fwd(CTest::minVal);
}

对于这个调用TargetFun(CTest::minVal),编译器直接做常数替换。

如果创建了指针或者引用(指针和引用本质上是同一事物)指向这类变量,那么就代表有寻址操作,也就意味着为其分配内存,所以必须提供该变量的定义,否则在链接期报错——应该是未定义的符号

回归到本例,对于这个调用fwd(CTest::minVal),fwd的形参是个通用引用,那么也就意味着必须有定义。本例我们并没有给出定义,最后就导致调用目标函数成功,通用引用模板函数失败的情况。

值得提出的是上述例子能链接成功,这取决于具体的编译器和链接器,如果遇到失败的情况我们只需在cpp文件中增加其定义,同时为了增加代码可移植性我们还是按照通用的写法吧

重载函数和模板作为通用引用函数模板实参


当重载函数和模板作为通用引用函数模板实参也会导致完美转发失败,我们把目标函数修改一下:

void TargetFun(int (*pFun)(int))
{
}

//void TargetFun(int pFun(int))
//{
//}

int ProcFun(int nVal);
int ProcFun(int nVal,int nKey);

{
	TargetFun(ProcFun);
	
	fwd(ProcFun);
}
  • 调用TargetFun(ProcFun)

    对于这个调用只是传递了函数名字ProcFun,这里是编译器帮我们找到了能够匹配目标函数形参的ProcFun(即:int ProcFun(int nVal))

  • 调用fwd(ProcFun)

    fwd是个通用引用函数模板,不严谨的说对于上层调用没有强类型限制,这就导致编译器不知道传递哪个具体的函数ProcFun,也就是没有具体类型,没有类型,模板的类型推导也就无从谈起

同样的问题也存在于向通用引用函数模板传递另一个函数模板名字,例如:

template<typename T>
T DoTemplate(T param)
{
	
}

{
	fwd(DoTemplate);
}

针对这两个完美转发失败的情况,我们也有解决方案(指定具体的重载函数,模板实例化):

using ProcFunType = int (*)(int);

ProcFunType pFun = ProcFun;

{
	fwd(pFun);
	fwd(static_cast<ProcFunType>(DoTemplate));
}

位域


位域是由机器字的若干任意部分组成的,C++规定,指针可以指向的最小实体是char,对于非const引用不得绑定到位域。可以传递的位域形参有两种:

  1. 位域值的副本按值传递

    被调用的函数收到位域内对应值的副本

  2. 常量引用

    常量引用其实也并不是真正绑定到位域,标准要求这时的引用实际绑定到存储在某种标准整型中的位域值副本,即绑定到常规对象,这个对象复制了位域的值

所以没有函数可以把位域绑定到引用,也不可能接受指向位域的指针

例如:

struct SIpv4Header
{
	std::uint32_t ver : 4, IHL : 4, DSCP : 6,ECN : 2, len : 16;
};
void TargetFun(std::size_t sz)
{
}

{
	SIpv4Header objIpv4;
	
	TargetFun(objIpv4.len);

	fwd(objIpv4.len);
}
  • 调用TargetFun(objIpv4.len)

    编译通过,实参按照位域值的副本按值传递

  • 调用fwd(objIpv4.len)

    编译不通过,通用引用模板触犯了把位域绑定到引用,此时我们可以创建一个位域值的副本以此解决完美转发失败问题:

      auto length = static_cast<std::uint16_t>(objIpv4.len);
      fwd(length);
    

完美转发失败情况有上述5种情况,每一种都给出了对应的解决方案,对于完美转发失败情况总结下来有两种原因:

  1. 对模板型别推导失败
  2. 推导结果是错误的结果