首页 > 代码库 > C++11实现placeholder

C++11实现placeholder

文章分析如何在C++11中实现简单的placeholder。


首先看看什么是placeholder:
for_each(arr.begin(), arr.end(), cerr << _0 << endl);
其中arr的类型是vector<int>,第三个参数是一个表达式,表达式中有一个操作数是_0。第三个参数含义是对这个表达式进行求值,得到一个对象x,x拥有operator ()(int a)的成员函数。整个表达式意味着把arr的每一个元素b取出来,然后调用x(b)。而x(b)的效果是将b输出到一行当中。其中的_0就是placeholder,在表达式中作为一个占位符存在,等待外部给出_0的类型和值的时候,整个表达式再求值。

这是个很酷的语法糖,在C++11中可以用lambda表达式代替,不过弄明白怎么实现对于模板元编程的能力会有所提升。下面来分析一下如何去实现placeholder:

考虑_0, _1这些占位符,首先搞明白它们在C++中的语法成份。出现在表达式中,而且表达式不是处于某个模板的环境,可以肯定在编译时能知道表达式的所有操作数的类型。

_0可以是一个形如被定义为make_placeholder0()的宏,也可以是T0 _0;之类的一个对象。不过这没有关系,我们只关心_0这个表达式的类型。可以看出_0应该具有类类型,operator << 能作用于该类型的对象上。占位符和其它操作数进行运算,会产生新的类型,新的类型还可以参加运算,最后使得cerr << _0 << endl这个表达式具有某个类型。我们假定这个产生的新的类型是TExpr,TExpr在运算后仍然是TExpr类型。如果把语法树的结点用类型标注一下,对于表达式_0 + _1 - _2应该是如下结果


                       - : TExpr
               + : TExpr      _2 : T2

         _0 : T0  _1 : T1


TExpr将拥有operator (),能够求值。但是目前看来TExpr上的operator () 具有不确定性:
根结点上,operator将使用两个操作数,把减法操作应用在两个操作数上。
在左边的结点上,是将加法应用在两个操作数上。

所以TExpr中的operator ()是多态的。如果将语法树改为:
                       - : TExpr<MinusTag>
               + : TExpr<AddTag>      _2 : T2
         _0 : T0  _1 : T1
事情就变得容易一些,TExpr是参数化的,根据不同的Tag参数,在operator ()时有不同的行为。

可以预见我们应该构造一堆这样的类:
template<typename L, typename R, typename T>
class TExpr;


template<typename L, typename R>
class TExpr<L, R, AddTag>
{
	L left_operand;
	R right_operand;
};


template<typename L, typename R>
class TExpr<L, R, SubTag>
{
	L left_operand;
	R right_operand;
};
其中L,R两个类型可能是T0,T1等占位符的类型,或者是TExpr<,,,>,或者是其它类型。但是这样做有个缺点,把参数个数写死了。于是将上面的AddTag和TExpr组合到一起,形成
template<typename T1, typename T2>
struct TExprAdd
{
};
template<typename T1, typename T2>
struct TExprSub
{
};
进一步,将_0和_1的类型定义为:
template<int v>
struct TExprBasic
{
};
并提供操作:
template<typename T1, typename T2>
TExprAdd<T1, T2> 
operator + (T1 a, T2 b)
{
	typedef TExprAdd<T1, T2> TI;
	return TI(a, b);
}


template<typename T1, typename T2>
TExprSub<T1, T2> 
operator - (T1 a, T2 b)
{
	typedef TExprSub<T1, T2> TI;
	return TI(a, b);
}
于是语法树变为:


                        - : TExprSub<TExprAdd<TExprBasic<0>, TExprBasic<1>>, TExprBasic<2>>
                + : TExprAdd<TExprBasic<0>, TExprBasic<1>>          _2 : TExprBasic<2>
     _0 : TExprBasic<0>  _1 : TExprBasic<1>

至此,我们已经给出了一个可以用的placeholder的架构了。

我们要求TExprAdd,TExprSub拥有一些共性,满足某个concept,这个concept就是TExpr。这个concept是自己在编程中心中默默建立的,当然也可以把这个concept用显式的方式写出来:
<pre name="code" class="cpp">template<typename T>
class TExpr
{
	// require 1:T具有计算返回类型的元函数get_result_type
	template<typename TUPLE>
	struct get_result_type
	{
		typedef typename T::template get_result_type<TUPLE> Impl;
		typedef typename Impl::result_type result_type;
	};
	
	// require 2:T的对象应该具有operator () 成员函数模板,用于求值
	template<typename... Arg>
	auto operator () (Arg... arg)->typename get_result_type<std::tuple<Arg...>>::result_type
	{
		return impl.template operator () (std::forward<std::tuple<TL...>>(t));
	}
	
	T impl;
};

template<typename T1, typename T2>
TExpr<TExprAdd<T1, T2> >
operator + (T1 a, T2 b)
{
	typedef TExprAdd<T1, T2> TI;
	return TExpr<TI>(TI(a, b));
}

template<typename T1, typename T2>
TExpr<TExprSub<T1, T2> >
operator - (T1 a, T2 b)
{
	typedef TExprSub<T1, T2> TI;
	return TExpr<TI>(TI(a, b));
}

这样就相当于描述了一个TExpr接口,有若干个具体实现:TExprBasic,TExprAdd,TExprSub等。[在这里接口的描述体现在TExpr对模板参数类型的依赖,要求T包含什么样的类型,要求T的对象有什么样的操作。接口的实现体现在把TExpr<...>作为TExpr的模板参数。]嗯,也就是表达式模板技术。于是我们的语法树中的-和+结点的类型分别变为:

TExpr<TExprAdd<TExprBasic<0>, TExprBasic<1>>>和TExpr<TExprSub<TExprAdd<TExprBasic<0>, TExprBasic<1>>, TExprBasic<2>>>增加TExpr好处是,语法树中的结点被分为三类:TExpr<...>,TExprBasic<...>,以及其它。这三类结点要求能够计算返回类型,求值。首先明确计算的输入和输出,返回类型的输入是变长模板参数,返回一个返回类型,求值的输入是tuple,返回一个值。而三种类型的编译时和运行时求值策略很清楚:1. TExpr可以直接依赖于模板参数,计算返回值类型,求值。2. TExprBasic可以直接计算返回值类型(输入变长模板参数,返回该模板参数某个位置上的类型),直接求值(输入tuple,返回tuple中某一项)。

3. 其它类型的计算返回值类型,求值方法就是本身的类型和值。

所以可以非常容易地针对这三类结点写出处理类。另外还有TExprAdd之类的类没有处理。这类结点的返回值类型计算需要分别计算其左操作数和右操作数的类型,然而在C++11中我们可以方便地用decltype(TA()+TB())来进行处结果的类型推导。而求值也是分别计算左右操作数的值,然后中间填上一个对应的操作符,作为一个表达式返回。[另外,还可以把TExprBasic作为TExpr的参数,这样所有的结点类型只剩下TExpr和其它类型了,更加方便管理。这个包装过程只需要改改代码即可。不过不改也能体现出一种思想,TExpr是递归的,TExprBasic是原子的,其它类型是原子的。]在此基础上,还引入两类print函数,没有参数的能输出带占位符的语法树,有参数的能输出带更多信息的语法树。

#include <iostream>
#include <tuple>
#include <vector>
#include <algorithm>
using namespace std;

template<int v>
struct int_{enum{value=http://www.mamicode.com/v};};>