当需要调试模板的时候,我们将会面临来自两方面的挑战。一种挑战来自模板的编写者:针对某一个模板, 如果它的任何一个模板实参都已符合文档所编写的要求,那么我们如何才能够保证模板可以正确地运作呢?另一种挑战正好来自对方的一方(即模板的使用者):在遇到模板的行为和文档中所描述的情况有差异的时候,模板的用户如何才能发现哪个模板参数违反了文档要求,或者是违反了模板参数的哪条要求呢?
在深入讨论这些话题之前,让我们先来考察可以强加给模板参数的一些约束,这是非常有必要的。因为在这一节里,我们叙述的大多数编译期错误就是由于违反这些约束而产生的,我们把这些约束称为语法约束(syntactic constraint)。语法约束可以包括要求某种构造函数必须存在、某个特定函数调用不能产生二义性等。而对于其他的约束,我们称为语义约束(semantic constraint)。事实上,要想机械地验证某个约束究竟属于哪一类约束是非常困难的;在有些情况下,我们甚至根本就不能够进行这种验证。例如,我们会要求模板类型参数必须具有operator<的定义(这是个语法约束),但大多数情况下,我们还要求这个运算符实际上定义的是某种领域下的排序规则(这是语义约束)。
concept这个术语通常被用于表示:在模板库中重复需求的约束集合。例如,C++标准库就依赖于诸如随机访问迭代(random access iterator)和缺省可构造(default constructible)等concept。concepts还可以形成体系:就是说,某个concept可以是其他concept的进一步细化(也称为精化),更精化的concept不但具备上层concept的各种约束,而且还增加了一些针对自身的约束。例如,在C++标准程序库中,concept random access iterator就是concept bidirectional iterator的精化。有了这些术语之后,在模板实现和模板使用的过程当中,我们可以认为:调试模板代码的主要工作是判断模板实现和模板定义中哪些concept被违反了。
浅式实例化
先看下我们自己写的代码:
template <typename T>
void clear(T const &p)
{
*p = 0; //假设T是一个类似指针的类型
}
template <typename T>
void core(T const &p)
{
clear(p);
}
template <typename T>
void middle(typename T::Index p)
{
core(p);
}
template <typename T>
void shell(T const &env)
{
typename T::Index i;
middle<T>(i);
}
class Client{
public:
typedef int Index;
};
Client main_client;
int main()
{
shell(main_client);
}
这个例子给出了软件开发中典型的层次结构:诸如shell()的高层函数模板依赖于诸如middle()的组件,而组件又使用了诸如core()的功能。当我们实例化shell()的时候,它下面层次的函灵敏模板也应该相应地被实例化。在这个例子中,我们的最底层发现了一个问题:我们用int类型对core()进行实例化(int来自于middle()中Client::Index的使用),并且试图对一个int类型的值解引用(dereference),而这明显是错误的。另外,一份完整的通用诊断信息应该包含对产生这个问题的所有层次的跟踪信息,但我们往往会发现根本很难准确把握这么多的信息。
在[StroustrupDnE]中我们可以找到一些围绕这个问题的详细讨论,Biarne Stroustrup提出了两种可以提前验证模板实参是否符合一系列约束的方法:通过语言扩展或者通过提前使用参数。我们将在13.11节对前一种方法进行介绍:而后一种方法主要在于把模板错误强制在浅式实例化中。我们是通过插入没有被使用的代码来获取这种实现的,这些代码并没有其他的用途,只是在实例化模代码的高层模板实参不符合低层模板约束时,引发一个错误。
在我们前面的例子中,我们可以在shell()中添加代码,让它试图对T::Index类型的值 进行解引用。例如:
template <typename T>
inline void ignore(T const &)
{
}
template <typename T>
void shell(T const &env)
{
class ShallowChecks {
void deref(T::Index ptr) {
ignore(*ptr);
}
};
typename T::Index i;
middle(i);
}
如果T是一个使T::Index不能被解引用的类型,那么在局部类ShallowChecks将会引发一个错误。另外我们知道,实际上这个局部类并没有被使用(即哑代码),因此添加的代码并不会影响shell()函数的运行时间。但遗憾的是,许多编译器都会对ShallowChecks并没有被使用的这个事实(它的成员也没有被使用)提出警告。我们可以使用诸如ignore()模板等tricks来避免这类警告,但却会增加代码的复杂度。
显然,在我们的例子中所开发的这种哑代码会和实出模板实际功能的代码一样复杂。为了控制这种复杂度,我们需要收集各种哑代码片断来组成一个程序库。譬如,这样一个程序库应该包含了许多可以扩展成代码的宏,当模板的参数替换违反了参数替换的concept时,这些代码就会引发一个错误。就目前而言,最流行的这类程序库是Concept Check Library,它属于Boost库发布产品的一部分。
遗憾的是,这些技术的可移植性很差(不同的编译器对错误的诊断方法并不一致),而且,有时候还掩饰了一些在更高层次不能被捕获的错误。
跟踪程序
到目前为止,我们已经讨论了编译和链接包含模板的程序时所出现的错误。然而,最具挑战性的任务在于:在确认程序可以正确运行之前,我们先要确认程序的创建过程也是成功的。模板通常都会令创建过程更加复杂,因为模板所表示的通用代码还要依赖于使用模板的客户端(这也是比普通类、普通函数多的地方)。跟踪程序(tracer)是一个软件设备,它通过开发周期的早期检测模板定义中的问题,来减累调试时各个方面的负担。
跟踪程序可以是一个用户定义的类,可以用做一个测试模板的实参。通常,该类的定义有且仅有满足模板测试的功能。更重要的是,对于跟踪程序所调用的每个操作,该跟踪程序都应该产生一个针对该操作的跟踪。例如,利用跟踪程序,我们可以用实验方法来确认算法的效率和操作的实际调用步骤。
下面是一个利用跟踪程序来测试排序算法的例子:
//tracer.h
#ifndef TRACER_H
#define TRACER_H
#include <iostream>
class SortTracer {
private:
int value; //要被排序的整数值
int generation; //产生拷贝的份数
static long n_created; //调用构造函数的次数
static long n_destroyed; //调用析构函数的次数
static long n_assigned; //赋值的次数
static long n_compared; //比较的次数
static long n_max_live; //现存对象的最大个数
//重新计算现存对象的最大小数
static void update_max_live()
{
if(n_created - n_destroyed > n_max_live) {
n_max_live = n_created - n_destroyed;
}
}
public:
static long creations() {
return n_created;
}
static long destructions() {
return n_destroyed;
}
static long assignments() {
return n_assigned;
}
static long comparisons() {
return n_compared;
}
static long max_live() {
return n_max_live;
}
public:
//构造函数
SortTracer(int v = 0): value(v), generation(1) {
++n_created;
update_max_live();
std::cerr << "SortTracer #" << n_created
<< ", created generation " << generation
<< " (total: " << n_created - n_destroyed
<< ")\n";
}
//拷贝构造函数
SortTracer(SortTracer const &b)
:value(b.value), generation(b.generation + 1){
++n_created;
update_max_live();
std::cerr << "SortTracer #" << n_created
<< ", copied as generation " << generation
<< " (total: " << n_created - n_destroyed
<< ")\n";
}
//析构函数
~SortTracer() {
++n_destroyed;
update_max_live();
std::cerr << "SortTracer generation " << generation
<< " destroyed (total: "
<< n_created - n_destroyed << ")\n";
}
//赋值运算符
SortTracer& operator=(SortTracer const &b) {
++n_assigned;
std::cerr << "SortTracer assignment #" << n_assigned
<< " (generation " << generation
<< " = " << b.generation
<< ")\n";
return *this;
}
//比较运算符
friend bool operator < (SortTracer const &a, SortTracer const &b){
++n_compared;
std::cerr << "SortTracer comparison #" << n_compared
<< " (generation " << a.generation
<< " < " << b.generation
<< ")\n";
return a.value < b.value;
}
int val() const {
return value;
}
};
#endif // TRACER_H
除了排序值value之外,这个tracer类还提供了几个用来跟踪实际排序过程的成员:generation跟踪原有对象产生了多少份拷贝。其他的静态成员分别跟踪:
创建的个数(构造函数调用的次数)、析构函数调用的次数、赋值运算符调用的次数、比较的次数以及同一时刻现存对象的最大个数。
下面的静态成员定义在一个分开的dot-C文件中:
//traler.cpp
#include "tracer.h"
long SortTracer::n_created = 0;
long SortTracer::n_destroyed = 0;
long SortTracer::n_max_live = 0;
long SortTracer::n_assigned = 0;
long SortTracer::n_compared = 0;
这个特殊的跟踪程序(tracer)类让我们能够跟踪给定模板的模式、实体创建、析构函数、赋值操作和比较操作。下面的测试程序针对C++标准库的std::sort算法来说明这一系列跟踪:
//tracertest.cpp
#include <algorithm>
#include "tracer.h"
int main()
{
//准备输入的例子
SortTracer input[] = {7, 3, 5, 6, 4, 2, 0, 1, 9, 8};
//输出初始值
for(int i = 0; i < 10; ++i) {
std::cerr << input[i].val() << ' ';
}
std::cerr << std::endl;
//存取初始状态
long created_at_start = SortTracer::creations();
long max_live_at_start = SortTracer::max_live();
long assigned_at_start = SortTracer::assignments();
long compared_at_start = SortTracer::comparisons();
//执行算法
std::cerr << "---[ Start std::sort() ]-------------\n";
std::sort<>(&input[0], &input[9] + 1);
std::cerr << "---[ End std::sort() ]---------------\n";
//确认结果
for(int i = 0; i < 10; ++i) {
std::cerr << input[i].val() << ' ';
}
std::cerr << "\n\n";
//最终报告
std::cerr << "std::sort() of 10 SortTracer's"
<< " was performed by:\n "
<< SortTracer::creations() - created_at_start
<< " temporary tracers\n "
<< "up to "
<< SortTracer::max_live()
<< " tracers at the same time ("
<< max_live_at_start << " before)\n "
<< SortTracer::assignments() - assigned_at_start
<< " assignments\n "
<< SortTracer::comparisons() - compared_at_start
<< " comparisons\n\n";
return 0;
}
SortTracer #1, created generation 1 (total: 1)
SortTracer #2, created generation 1 (total: 2)
SortTracer #3, created generation 1 (total: 3)
SortTracer #4, created generation 1 (total: 4)
SortTracer #5, created generation 1 (total: 5)
SortTracer #6, created generation 1 (total: 6)
SortTracer #7, created generation 1 (total: 7)
SortTracer #8, created generation 1 (total: 8)
SortTracer #9, created generation 1 (total: 9)
SortTracer #10, created generation 1 (total: 10)
7 3 5 6 4 2 0 1 9 8
---[ Start std::sort() ]-------------
SortTracer #11, copied as generation 2 (total: 11)
SortTracer comparison #1 (generation 2 < 1)
SortTracer comparison #2 (generation 1 < 2)
SortTracer assignment #1 (generation 1 = 1)
SortTracer assignment #2 (generation 1 = 2)
SortTracer generation 2 destroyed (total: 10)
SortTracer #12, copied as generation 2 (total: 11)
SortTracer comparison #3 (generation 2 < 1)
SortTracer comparison #4 (generation 1 < 2)
SortTracer assignment #3 (generation 1 = 1)
SortTracer assignment #4 (generation 1 = 1)
SortTracer assignment #5 (generation 1 = 2)
SortTracer generation 2 destroyed (total: 10)
SortTracer #13, copied as generation 2 (total: 11)
SortTracer comparison #5 (generation 2 < 1)
SortTracer comparison #6 (generation 1 < 2)
SortTracer assignment #6 (generation 1 = 1)
SortTracer assignment #7 (generation 1 = 1)
SortTracer assignment #8 (generation 1 = 1)
SortTracer assignment #9 (generation 1 = 2)
SortTracer generation 2 destroyed (total: 10)
SortTracer #14, copied as generation 2 (total: 11)
SortTracer comparison #7 (generation 2 < 1)
SortTracer comparison #8 (generation 1 < 2)
SortTracer assignment #10 (generation 1 = 1)
SortTracer assignment #11 (generation 1 = 1)
SortTracer assignment #12 (generation 1 = 1)
SortTracer assignment #13 (generation 1 = 1)
SortTracer assignment #14 (generation 1 = 2)
SortTracer generation 2 destroyed (total: 10)
SortTracer #15, copied as generation 2 (total: 11)
SortTracer comparison #9 (generation 2 < 1)
SortTracer comparison #10 (generation 1 < 2)
SortTracer assignment #15 (generation 1 = 1)
SortTracer assignment #16 (generation 1 = 1)
SortTracer assignment #17 (generation 1 = 1)
SortTracer assignment #18 (generation 1 = 1)
SortTracer assignment #19 (generation 1 = 1)
SortTracer assignment #20 (generation 1 = 2)
SortTracer generation 2 destroyed (total: 10)
SortTracer #16, copied as generation 2 (total: 11)
SortTracer comparison #11 (generation 2 < 1)
SortTracer comparison #12 (generation 1 < 2)
SortTracer assignment #21 (generation 1 = 1)
SortTracer assignment #22 (generation 1 = 1)
SortTracer assignment #23 (generation 1 = 1)
SortTracer assignment #24 (generation 1 = 1)
SortTracer assignment #25 (generation 1 = 1)
SortTracer assignment #26 (generation 1 = 1)
SortTracer assignment #27 (generation 1 = 2)
SortTracer generation 2 destroyed (total: 10)
SortTracer #17, copied as generation 2 (total: 11)
SortTracer comparison #13 (generation 2 < 1)
SortTracer comparison #14 (generation 1 < 2)
SortTracer assignment #28 (generation 1 = 1)
SortTracer assignment #29 (generation 1 = 1)
SortTracer assignment #30 (generation 1 = 1)
SortTracer assignment #31 (generation 1 = 1)
SortTracer assignment #32 (generation 1 = 1)
SortTracer assignment #33 (generation 1 = 1)
SortTracer assignment #34 (generation 1 = 1)
SortTracer assignment #35 (generation 1 = 2)
SortTracer generation 2 destroyed (total: 10)
SortTracer #18, copied as generation 2 (total: 11)
SortTracer comparison #15 (generation 2 < 1)
SortTracer comparison #16 (generation 2 < 1)
SortTracer assignment #36 (generation 1 = 2)
SortTracer generation 2 destroyed (total: 10)
SortTracer #19, copied as generation 2 (total: 11)
SortTracer comparison #17 (generation 2 < 1)
SortTracer comparison #18 (generation 2 < 1)
SortTracer comparison #19 (generation 1 < 2)
SortTracer assignment #37 (generation 1 = 1)
SortTracer comparison #20 (generation 2 < 1)
SortTracer assignment #38 (generation 1 = 2)
SortTracer generation 2 destroyed (total: 10)
---[ End std::sort() ]---------------
7 3 5 6 4 2 0 1 9 8
std::sort() of 10 SortTracer's was performed by:
9 temporary tracers
up to 11 tracers at the same time (10 before)
38 assignments
20 comparisons
SortTracer generation 1 destroyed (total: 9)
SortTracer generation 1 destroyed (total: 8)
SortTracer generation 1 destroyed (total: 7)
SortTracer generation 1 destroyed (total: 6)
SortTracer generation 1 destroyed (total: 5)
SortTracer generation 1 destroyed (total: 4)
SortTracer generation 1 destroyed (total: 3)
SortTracer generation 1 destroyed (total: 2)
SortTracer generation 1 destroyed (total: 1)
SortTracer generation 1 destroyed (total: 0)
运行这个程序我们将会看到多行的输出,但从“最后的输出报告”这一行开始我们可以得到所期望的结论。针对std::sort()函数的实现,我们可以得到下面的输出报告:譬如,我们在例子中可以看到:在排序的时候,虽然创建了15个临时的tracer,但在同一时廖最多只存在两个多余的tracer.因此,我们的tracer扮演的两种角色:它说明了我们的tracer完全满足标准sort()算法的要求(例如,并不需要运行符==和运算符>),另外,它让我们对算法的开销有个大体的把握。然而,它并没有给出排序模板的正确性究竟如何。
oracles
使用跟踪程序是相对比较简单和行之有效的技术,但它只能让我们对模板的特定输入和相关功能的特定行为跟踪。然而,
我们可能会期望跟踪程序能够处理并不局限于这些特定要求的其他情史。例如,用于排序算法的比较运算符需要具备什么条件,才能够使比较是有效的(或者是正确的)。但在例子中,我们只是对整数和小于号情况进行了测试,在测试条件下,该比较运算符是有效的,但对其他的情况,该运算符是否仍然有效则一概不知。
在某些领域,tracer的一个扩展版本被称为oracles(或称为run-time analysis oracles)。它们是连接到推理引擎的tracers————所谓推理引擎(inference engine)是一个程序,它可以记住用来推导出结论的断言和推理。有一个被应用于标准库某一部分的这种系统,它的名了叫MELAS, [MusserWangDynaVeri]对它有详细的讨论。
在某些情况下,利用oracles,我们可以动态地验证模板算法,而不需要完全指定作为替换的模板实参(oracles本身就是实参),也不需要指定输入数据(当程序由于缺少输入数据而不能继续时,推理引擎会请求某种输入假设)。然而,在这种方式下,对算法复杂度的分析也是相当有限的(由于推理引擎的不足),而且工作量是巨大的。基于这些原因,我们并不深入研究oracles,
但是有兴趣的读者可以参考前面所提到的那本书(和书中所列的书目)。
archetypes
我们前面提到:tracers通常提供了一个接口,它是所跟踪模板需要具备的最小接口。当“只具备这个接口的tracer”并不产生运行期输出的时候,我们有时把这种tracer称为archetype(原型)。利用archetype,我们可以验证一个模板实现是否会请求期望之外的语法约束。
典型而言,一个模板的实现可能会为模板库中标记的每个concept,都开发一个archetype。