0 论抽象——前言
故事要从一个看起来非常简单的功能开始:
请计算两个数的和。
如果你对Python很熟悉,你一定会觉得:“哇!这太简单了!”,然后写出以下代码:
def Plus(lhs, rhs):
return lhs + rhs
那么,C语言又如何呢?你需要面对这样的问题:
/* 这里写什么?/ Plus(/ 这里写什么?/ lhs, / 这里写什么?*/ rhs)
{
return lhs + rhs;
}
也许你很快就能想到以下解法中的一些或全部:
硬编码为某个特定类型:
int Plus(int lhs, int rhs)
{
return lhs + rhs;
}
显然,这不是一个好的方案。因为这样的Plus函数接口强行的要求两个实参以及返回值的类型都必须是int,或是能够发生隐式类型转换到int的类型。此时,如果实参并不是int类型,其结果往往就是错误的。请看以下示例:
int main()
{
printf("%d\n", Plus(1, 2)); // 3,正确
printf("%d\n", Plus(1.999, 2.999)); // 仍然是3!
}
针对不同类型,定义多个函数
int Plusi(int lhs, int rhs)
{
return lhs + rhs;
}
long Plusl(long lhs, long rhs)
{
return lhs + rhs;
}
double Plusd(double lhs, double rhs)
{
return lhs + rhs;
}
// …
这种方案的缺点也很明显:其使得代码写起来像“汇编语言”(movl,movq,…)。我们需要针对不同的类型调用不同名称的函数(是的,C语言也不支持函数重载),这太可怕了。
使用宏
#define Plus(lhs, rhs) (lhs + rhs)
这种方案似乎很不错,甚至“代码看上去和Python一样”。但正如许许多多的书籍都讨论过的那样,宏,不仅“抛弃”了类型,甚至“抛弃”了代码。是的,宏不是C语言代码,其只是交付于预处理器执行的“复制粘贴”的标记。一旦预处理完成,宏已然不再存在。可想而知,在功能变得复杂后,宏的缺点将会越来越大:代码晦涩,无法调试,“莫名其妙”的报错…
看到这里,也许你会觉得:“哇!C语言真烂!居然连这么简单的功能都无法实现!”。但请想一想,为什么会出现这些问题呢?让我们回到故事的起点:
请计算两个数的和。
仔细分析这句话:“请计算…的和”,意味着“加法”语义,这在C语言中可以通过“+”实现(也许你会联想到汇编语言中的加法实现);而“两个”,则意味着形参的数量是2(也许你会联想到汇编语言中的ESS、ESP、EBP等寄存器);那么,“数”,意味着什么语义?C语言中,具有“数”这一语义的类型有十几种:int、double、unsigned,等等,甚至char也具有“数”的语义。那么,“加法”和“+”,“两个”和“形参的数量是2”,以及“数”和int、double、unsigned等等之间的关系是什么?
是抽象。
高级语言的目的,就是对比其更加低级的语言进行抽象,从而使得我们能够实现更加高级的功能。抽象,是一种人类的高级思维活动,是一种充满着智慧的思维活动。汇编语言抽象了机器语言,而C语言则进一步抽象了汇编语言:其将汇编语言中的各种加法指令,抽象成了一个简单的加号;将各种寄存器操作,抽象成了形参和实参…抽象思维是如此的普遍与自然,以至于我们往往甚至忽略了这种思维的存在。
但是,C语言并没有针对类型进行抽象的能力,C语言不知道,也没有能力表达“int和double都是数字”这一语义。而这,直接导致了这个“看起来非常简单的功能”难以完美的实现。
针对类型的抽象是如此重要,以至于编程语言世界出现了与C语言这样的“静态类型语言”完全不一样的“动态类型语言”。正如开头所示,在Python这样的动态类型语言中,我们根本就不需要为每个变量提供类型,从而似乎“从根本上解决了问题”。但是,“出来混,迟早要还的”,这种看似完美的动态类型语言,牺牲的却是极大的运行时效率!我们不禁陷入了沉思:真的没有既不损失效率,又能对类型进行抽象的方案了吗?
正当我们一筹莫展,甚至感到些许绝望之时,C++的模板,为我们照亮了前行的道路。
1 新手村——模板基础
1.1 函数模板与类模板
模板,即C++中用以实现泛型编程思想的语法组分。模板是什么?一言以蔽之:类型也可以是“变量”的东西。这样的“东西”,在C++中有二:函数模板和类模板。
通过在普通的函数定义和类定义中前置template <…>,即可定义一个模板,让我们以上文中的Plus函数进行说明。请看以下示例:
此为函数模板:
template
T Plus(T lhs, T rhs)
{
return lhs + rhs;
}
int main()
{
cout << Plus(1, 2) << endl; // 3,正确!
cout << Plus(1.999, 2.999) << endl; // 4.998,同样正确!
}
此为类模板:
template
struct Plus
{
T operator()(T lhs, T rhs)
{
return lhs + rhs;
}
};
int main()
{
cout << Plus()(1, 2) << endl; // 3,正确!
cout << Plus()(1.999, 2.999) << endl; // 4.998,同样正确!
}
显然,模板的出现,使得我们轻而易举的就实现了类型抽象,并且没有(像动态类型语言那样)引入任何因为此种抽象带来的额外代价。
1.2 模板形参、模板实参与默认值
请看以下示例:
template
struct Plus
{
T operator()(T lhs, T rhs)
{
return lhs + rhs;
}
};
int main()
{
cout << Plus()(1, 2) << endl;
cout << Plus()(1.999, 2.999) << endl;
}
上例中,typename T中的T,称为模板形参;而Plus中的int,则称为模板实参。在这里,模板实参是一个类型。
事实上,模板的形参与实参既可以是类型,也可以是值,甚至可以是“模板的模板”;并且,模板形参也可以具有默认值(就和函数形参一样)。请看以下示例:
template <typename T, int N, template <typename U, typename = allocator> class Container = vector>
class MyArray
{
Container __data[N];
};
int main()
{
MyArray<int, 3> _;
}
上例中,我们声明了三个模板参数:
typename T:一个普通的类型参数
int N:一个整型参数
template <typename U, typename = allocator> class Container = vector:一个“模板的模板参数”
什么叫“模板的模板参数”?这里需要明确的是:模板、类型和值,是三个完全不一样的语法组分。模板能够“创造”类型,而类型能够“创造”值。请参考以下示例以进行辨析:
vector v;
此例中,vector是一个模板,vector是一个类型,而v是一个值。
所以,一个“模板的模板参数”,就是一个需要提供给其一个模板作为实参的参数。对于上文中的声明,Container是一个“模板的模板参数”,其需要接受一个模板作为实参 。需要怎样的模板呢?这个模板应具有两个模板形参,且第二形参具有默认值allocator;同时,Container具有默认值vector,这正是一个符合要求的模板。这样,Container在类定义中,便可被当作一个模板使用(就像vector那样)。