一.前言
在之前,我曾花三篇文章讲述了C++委托机制的封装,到最后可以实现任意类型函数的捆绑,包括lambda表达式的注册。然而,这个委托机制还有一点需要完善,可能看标题大家知道这个需要完善的点儿是什么,不过不知道也关系,这个文章会由浅入深的讲述这个问题的来源,以及解决方案。
二.问题引入
在直接上委托代码之前我们先抽出问题本质,用简单的代码来发现问题,例子来源于<C++ primer>P612:
这个将编写一个Agent函数,它负责接受一个函数和两个参数,并且用这个函数来调用这两个参数。
template<typename Function,typename Param1,typename Param2>
void Agent(Function f,Param1 p1,Param2 p2){
f(p1,p2);
}
这个函数就相当于委托的一个雏形,一般情况下,这个函数能工作得很好,然而在当f的实例是一个接受引用参数的函数就会出现问题,因为无论左右值在模板的类型推演的过程中是不会带引用的。例如:
template<typename T>
void f(T a){}
f(1) -> T为int
int a;
f(a) -> T为int
如何解决这个问题呢?这里有两种方案:
1.使用move()或者ref()来”提醒”模板推演成引用类型。
class A
{
public:
A() { cout << "构造" << endl; }
A(const A&) { cout << "拷贝" << endl; }
A(A&&) { cout << "移动" << endl; }
};
template<typename T>
void X(T a) {
}
int main()
{
A a;
X(ref(a));
X(move(a));
return 0;
}
以上方法虽然在这种情况下能够解决问题,然而在实际过程中还是会有许多局限性:
- 在参数转发路径上始终要记住保持参数类型。也就是如果转发层数过多,那么参数类型在中途可能还是会有部分修饰丢失。
- 调用形式始终要和函数参数类型保持一致,不利于维护。
那么,我们就比较希望能够有一种方式能够使得参数在转发的路途中始终保持自身的完整类型。而这个方法就是forward.
三.引用折叠
在讲forward之前需要引入一个新的概念——引用的折叠。
我们知道在C++语法中规定不能定义引用的引用。然而,可能有部分同学写过如下代码:
typedef int& rint;
int a = 5;
rint b = a;
rint& c = b;
而且,这段代码是编译通过的,可能有人就会说,经过换名之后 rint&就是引用的引用。
然而实际上对于编译器而言rint& 实质就是 int& & <——请注意,两个&之间有空格。
根据不同的换名可能会有以下几种情况:
- T& &
- T&& &
- T& &&
- T&& &&
然而对于这些情况,编译器会发生引用折叠,如下:
原型 | 折叠后 |
---|---|
T& & | T& |
T&& & | T& |
T& && | T& |
T&& && | T&& |
所以说利用模板推演和引用折叠可以找到参数的真正类型。
四.forward的使用——prefect forward
首先我们来看forward的源码
template<class _Ty> inline
constexpr _Ty&& forward(
typename remove_reference<_Ty>::type& _Arg) _NOEXCEPT
{ // forward an lvalue as either an lvalue or an rvalue
return (static_cast<_Ty&&>(_Arg));
}
template<class _Ty> inline
constexpr _Ty&& forward(
typename remove_reference<_Ty>::type&& _Arg) _NOEXCEPT
{ // forward an rvalue as an rvalue
static_assert(!is_lvalue_reference<_Ty>::value, "bad forward call");
return (static_cast<_Ty&&>(_Arg));
}
可见forward是有两个版本的,下面给出上面英文的注解的翻译:
- 将一个左值实参按照其真实类型(左值或者右值)转发(转换)。
- 将一个右值按照右值类型转发(转换)。
由于篇幅原因我就只剖析第一个重载版本。剖析之前,我还是先要介绍forward的使用。
虽然forward是模板函数,参数可以自动推演,然而forward的参数是一个remove_reference萃取后的类型,也就是我们可以推演出remove_reference<_Ty>::type,但是无法推演出_Ty。
所以使用过程中,我们还是要显式给出_Ty;例如: forward< T >(obj)。
所以_Ty就是我们提供的类型T。
至于remove_reference< T > 作用就是去掉类型T的所有引用修饰。也就是说假如T为Type&或者Type&&,那么remove_reference< T >::type就是Type。这个模板是标准库提供的标准类型转换模板,为了方便阅读,我将其余的一些贴在了文章的最末。
根据上一段讲述的引用折叠我们知道_Ty&&折叠后就是_Ty的真正类型。
所以forward就是将左值实参按照给定的类型T进行强转。
了解了forward实现之后,我们来改写上面的Agent函数:
template<typename Function,typename Param1,typename Param2>
void Agent(Function f,Param1&& p1,Param2&& p2){
f(forward<Param1>(p1),forward<Param2>(p2));
}
这样,当我将左值int传入第一个参数的时候,Param1会根据引用折叠原理自动推演为int&而在调用f的过程中使用forward< Param1 >(p1)将p1保持Param1类型。
根据这个我改写了委托的代码,如下(在这里使用了模板参数包的展开技巧):
template<typename T>
class Delegate
{
};
template<typename Return, typename...Params>
class Delegate<Return(Params...)>
{
public:
typedef Return (*FunType) (Params...);
Delegate(FunType fun) :_fun(fun) {}
Return operator () (Params... params) {
return _fun(forward<Params>(params)...); //注意:这里使用了模板参数包的展开技巧
}
private:
FunType _fun;
};
void fun(int&& a) { cout << a << endl; }
int main()
{
Delegate<void(int&&)> a(fun);
a(2);
return 0;
}
*五.标准类型转换模板
名称 | 若T为 | 则Mod< T >::type为 | 否则Mod< T >::type为 |
---|---|---|---|
remove_reference | X&或者X& | X | T |
add_const | X&、const X 或者函数 | T | const T |
add_lvalue_reference | X&或者X&& | X& | T& |
add_rvalue_reference | X&或者X&& | X&& | T&& |
remove_pointer | X* | X | T |
add_pointer | X&或者X&& | X* | T* |
make_signed | unsigned X | X | T |
make_unsigned | 带符号类型 | unsigned X | T |
remove_extent | X[n] | X | T |
remove_all_extents | X[n1][n2]… | X | T |