目录:
C++ 下 Function 对象的实现(上)
C++ 下 Function 对象的实现(下)
起因在上一篇已经说过了。现在让我们直接进入主题。本文的目标是,让以下代码能顺利跑起来:
int intfun0()
{
return 1;
}
struct _intfunctor0
{
int operator()()
{
return 2;
}
} intfunctor0;
struct Test
{
int intmem0()
{
return 3;
}
} test;
int main()
{
Function<int ()> f1(&intfun0);
Function<int ()> f1_(intfun0);
Function<int ()> f2(intfunctor0);
Function<int ()> f3(&test, &Test::intmem0);
f1();
f1_();
f2();
f3();
return 0;
}
除了上述例子中显示的,还要支持有返回值的函数和没返回值的函数,以及有0个、1个、2个、……、MAX 个参数的函数,参数类型无限制。最后实现的 Function 对象仅仅可以执行就好。(至于是否可拷贝、是否可判断相等 等问题,都是小事,本文暂不考虑。)最后,Bind 概念也不在本文讨论范围之内。
对于这个问题,我们一开始考虑的可能是怎样统一三种不同形式。有两个选择,第一,使用 C++ 的多态机制,最后统一到基类指针的类型;第二,允许类内部有冗余变量以及必要的 Flag,用于判断是哪种形式的函数,要如何执行。这样看起来,第一种方案比第二种爽一点。于是,最初想到的实现有可能是这样的:
先定义一个虚基类:
template <typename R>
class FunctionBase0
{
public:
virtual R Invoke() = 0;
virtual ~FunctionBase0() {}
};
然后实现一个普通函数/仿函数的版本:
template <typename R, typename T>
class Function0 : public FunctionBase0<R>
{
public:
R Invoke()
{
return m_Fun();
}
public:
Function0(const T &fun)
: m_Fun(fun)
{
}
private:
T m_Fun;
};
这里需要说明的是,如果是普通函数,T会被特化成 R() 或者 R (&)() 或者 R(*)(),取决于使用的时候传入 fun 还是传入 &fun。所以不必另外实现针对 R(*)() 的版本。Loki (姑且就以作品名称乎 Loki 的作者吧,他那个真名实在是太长)在他的书中称之为“做一个,送一个”。不过对于他书中所说的,我有一个疑惑。Loki 说传入 fun,模版参数 T 会被特化成 R (&)(),于是一切顺利。可是我在操作过程中发现 T 一直被特化成 R (),于是上述 class 中的 m_Fun 被认为是成员函数而不是成员变量。不知道是为什么,有知道者请不吝指教哈。因为以上原因,本文中我一直用 &fun 的形式对待普通函数。
再实现一个成员函数的版本:
template <typename R, typename T>
class MemberFunction0 : public FunctionBase0<R>
{
public:
R Invoke()
{
return (m_pObj->*m_pMemFun)();
}
public:
MemberFunction0(T *pObj, R (T::*pMemFun)())
: m_pObj(pObj), m_pMemFun(pMemFun)
{
}
private:
R (T::*m_pMemFun)();
T *m_pObj;
};
最后是一个包装类。如果你可以接受 Function<int> 表示 int(), Function<int, int> 表示 int (int),…,那么这里没有多少技巧可言。boost 的那个 function 使用的是函数签名作为模版参数,即 Function<int()>,Function<int (int)> 等形式。如果不太研究语法,可能会像我一样,一开始会对尖括号里的 int (int) 之类的玩意儿不太熟悉,觉得很牛逼。可是了解了以后,不过是个函数类型而已,没什么大不了的。Loki 的 Functor 的使用方式是 Functor<int, TYPELIST_0()>,Functor<int, TYPELIST_1(int)>。其中第一个模版参数始终是返回值,第二个模版参数是参数类型列表,Loki 使用了他创造的玩意儿 TypeList 使得所有函数参数只占一个坑,这在等下的支持多参数的扩展中能够带来一些美观。我比较喜欢 boost 的使用方式,让使用者直接以语言规定的形式填入函数签名,而不是一些额外的约定(“第一个模版参数表示返回值”,“第二个到最后的模版参数表示参数”,“第二个模版参数以 TypeList 形式表示函数参数”等)。
为了达到这个目标,我们要玩一些偏特化技巧。关于偏特化,我一直以来的肤浅认识都是错误的。我原以为,对于模版类:
template <typename T0, typename T1>
class Foo;
我如果特化其中一个参数 T1:
template <typename T0>class Foo<T0, int>
{
}
我以为只有这样才叫偏特化,以为偏特化的过程总是减少模版参数的。而实际上,只要用某个/些类型占据原始模版参数的位置,就可以了。比如,对于上述 Foo,我可以特化一个 class<T0, std::map<U0, U1>>,消去一个 T1,而新增 U0、U1:
template <typename T0, typename U0, typename U1>class Foo<T0, std::map<U0, U1>>
{
}
原来 T1 的位置被 std::map<U0, U1> 占据了,这也是偏特化。当然最后的模版参数数量也可以不变,如:
template <typename T0, typename U>class Foo<T0, std::vector<U>>
{
}
以及
template <typename T0, typename U>class Foo<T0, U*>
{
}
其中后者是实现类型萃取的主要方式。只要特化以后,这个类依然带有至少一个模版参数,就是偏特化。如果最后产生了 template<> 的形式,那就是完全特化。
回到我们刚才的主题,我们要提供给用户的是这样一个类:
template <typename Signature>
class Function;
其中参数 Signature 会被实际的函数类型所特化。但是我们只知道整体的一个 Signature 并没有用,我们必须知道被分解开来的返回值类型、参数类型。于是,引入一个偏特化版本:
template <typename R>
class Function<R ()>
这里使用 R () 特化原始的 Signature,引入一个新的参数 R。于是返回值类型 R 就被萃取出来了。实现如下:
template <typename R>
class Function<R ()>
{
public:
template <typename T>
Function(const T &fun)
: m_pFunBase(new Function0<R, T>(fun))
{
}
template <typename T>
Function(T *pObj, R (T::*pMemFun)())
: m_pFunBase(new MemberFunction0<R, T>(pObj, pMemFun))
{
}
~Function()
{
delete m_pFunBase;
}
R operator ()()
{
return m_pFunBase->Invoke();
}
private:
FunctionBase0<R> *m_pFunBase;
};
如果对上面说的“普通函数的使用方式必须是函数指针而不是函数本身”耿耿于怀,可以再引入一个的构造函数:
typedef R (FunctionType)();
Function(const FunctionType &fun)
: m_pFunBase(new Function0<R, FunctionType &>(fun))
{
}
这里 FunctionType 是 R(&)() 类型,强制使用它来特化 Function0 中的 T。该构造函数在重载决议中会取得优先权从而使普通函数本身的传入成为可能。不过,以函数本身形式传入的普通函数会丧失一些特性,比如 Function<int()> 只能接受 int() 类型的普通函数而不能接受 char () 型的普通函数,因为这种情况下不会走我们刚才新定义的构造函数。
还有一种做法,就是针对全局函数,强制特化出模版参数为其引用类型的类。定义如下元函数:
template <typename Signature>
struct FunctionTraits
{
typedef Signature ParamType;
};
template <typename RetType>
struct FunctionTraits<RetType ()>
{
typedef RetType (&ParamType)();
};
然后构造函数改为:
template <typename T>Function(const T &fun)
: m_pFunBase(new Function0<R, typename FunctionTraits<T>::ParamType>(fun))
{
}
用以上方法,所有的特性都不会丢失。
到这儿,我们的 Function 已经可以小试牛刀了:
Function<int ()> f1(&intfun0);
Function<int ()> f2(intfunctor0);
Function<int ()> f3(&test, &Test::intmem0);
f1();
f1_();
f2();
f3();
上面这段代码已经能够正常运行了。
来,继续做一个,送一个。下面的代码居然也能跑(voidfun0、voidfunctor0、Test::voidmem0类似int版本定义):
Function<void ()> f4(&voidfun0);Function<void ()> f4_(voidfun0);
Function<void ()> f5(voidfunctor0);
Function<void ()> f6(&test, &Test::voidmem0);
f4();
f4_();
f5();
f6();
这说明了,在类里面写一个返回值为该类型的函数,并在里面写下 return XXX; 然后以 void 为模版参数传入该模版类,是符合语法的。验证一下:
template <typename T>
class Foo
{
public:
T Bar()
{
printf("%s invoked\n", __FUNCTION__);
return T();
}
};
int main()
{
Foo<void> f1;
f1.Bar();
Foo<int> f2;
int i = f2.Bar();
return 0;
}
运行结果:
Foo<void>::Bar invoked
Foo<int>::Bar invoked
到此为止,我们已经实现了 0 个参数的函数支持,也即 R () 类型的所有函数的支持。接下来还要实现对具有 1 个、2 个、3 个直至任意有限个参数的函数支持。也许您也发现了,接下来的工作可以是体力活,我们可以照葫芦画瓢,搞出一堆 FunctionBaseN、FunctionN、MemberFunctionN,并在最后的 Function 中再实现 N 个偏特化版本。是,不错,大致上原理就是这样。限于篇幅,我想暂时写到这里,下篇将继续谈谈宏、TypeList,以及怎样少花点力气实现其余 N 个版本。最终达到的效果是,只要改一个宏定义,就可以提高参数上限。
在本文所涉及的内容中,我比较纠结的是,可否在不用多态机制的情况下达到比较优雅的形式统一?
欢迎讨论。