模板参数可以通过类型(typename T)或值(int X)进行参数化。
“传统的”c++代码模板化方法是使用函子——也就是说,代码在一个对象中,对象因此赋予代码唯一的类型。
在处理传统函数时,这种技术不太适用,因为类型的变化并不表示特定的函数——相反,它只指定了许多可能函数的签名。所以:
template<typename OP>
int do_op(int a, int b, OP op)
{
return op(a,b);
}
int add(int a, int b) { return a + b; }
...
int c = do_op(4,5,add);
不等同于函子的情况。在本例中,do_op为签名为int X (int, int)的所有函数指针实例化。编译器必须非常积极地完全内联这种情况。(不过我不排除这种可能性,因为编译器优化已经非常先进了。)
有一种方法可以判断这段代码并不是我们想要的:
int (* func_ptr)(int, int) = add;
int c = do_op(4,5,func_ptr);
仍然是合法的,显然这个没有内联。为了获得完整的内联,我们需要按值创建模板,这样函数在模板中是完全可用的。
typedef int(*binary_int_op)(int, int); // signature for all valid template params
template<binary_int_op op>
int do_op(int a, int b)
{
return op(a,b);
}
int add(int a, int b) { return a + b; }
...
int c = do_op<add>(4,5);
在这种情况下,do_op的每个实例化版本都使用一个已经可用的特定函数进行实例化。因此,我们期望do_op的代码看起来很像“返回a + b”。(Lisp程序员们,别傻笑了!)
我们还可以确认这更接近我们想要的结果,因为:
int (* func_ptr)(int,int) = add;
int c = do_op<func_ptr>(4,5);
将编译失败。GCC说:“error: 'func_ptr'不能出现在常量表达式中。换句话说,我不能完全展开do_op,因为你在编译时没有给我足够的信息来知道我们的op是什么。
因此,如果第二个例子真的完全内联了我们的op,而第一个例子不是,那么模板有什么用呢?它在做什么?答案是:类型强制。这是第一个例子的重复:
template<typename OP>
int do_op(int a, int b, OP op) { return op(a,b); }
float fadd(float a, float b) { return a+b; }
...
int c = do_op(4,5,fadd);
这个例子是有用的!(我不是说它是好的c++,但是……)所发生的事情是do_op已经围绕各种函数的签名进行了模板化,并且每个单独的实例化将编写不同的类型强制代码。因此,使用fadd实例化do_op的代码看起来像这样:
convert a and b from int to float.
call the function ptr op with float a and float b.
convert the result back to int and return it.
通过比较,我们的By -value case要求函数参数精确匹配。