作者:Danny Kalev
编译:MTT 工作室
原文出处:Enforcing
Compile-time Constraints
摘要:实现某些约束需要太多的力气活,如果能找到某种通用方式实现那些更抽象的约束那就最好了,本文下面的解决方案将展示如何实现。 |
通用的约束和算法常常给所处理的对象强加某些限制。例如,std::sort()算法需要其操作的对象元素定义 <
操作符。强制此约束很容易:编译器尝试针对给定类型调用该操作符。如果这个操作符不存在,你会得到一个编译错误:#include <algorithm>
struct S{}; //doesn~t define operator <
int main()
{
S s[2];
std::sort(s, s+2); // 出错: 在类型 ~S~中 ~operator<~ 没有实现
}
但是,要表达约束以及强制执行约束并不是一件容易的事情。很多抽象的约束比如:“必须是一个基类”或“必须是一个 POD
类型”需要来自程序员更多的灵活性和技巧。本文下面将示范如何一通用的方式实现此类约束。 如何以通用的方式强制执行对象的编译时约束?
使用“约束模板”自动强制执行编译时约束 提出问题 假设你的应用程序需要一个接口,这个接口是用 C 或者 SQL 编写的非C++模块。为此,你需要保证传递到非C++模块的所有对象具备
POD 类型。
struct S1 { int x;};
class S2 { public: void func(); };
union S3 { struct { int x, y; } t; char c[4];};
struct S4 : S1, S2 {};
以上数据都是 POD 类型,而下面这些则不然:
struct C1 {
virtual void func(); //有一个虚函数
};
struct C2 {
struct T{ int x, y; };
~C2(); //有一个析构函数
};
struct C3 : virtual S1 {} ; //有一个虚拟基类
在个别编程实现中 POD 和 非 POD 的使用是有严格区分的,在<csstddef> 中定义的标准宏 offsetof()
就是一个例子。参见下列表达式:
size_t nbytes = offsetof (S, mem);
该表达式以字节为单位返回成员 mem 的偏移量。按照 C++ 标准,S 必须是一个 POD 类(class),结构(struct)或者联合(union),否则,结果是不确定的。所以你的任务是编写一个约束,这个约束能在编译时自动区分
POD 和 非 POD 类型。一旦违反了“必须是一个 POD 类型”约束,编译器便会发出明确的出错信息。
实现约束
约束实际上就是在某个类模板的成员函数中的一个表达式或者是一个声明。当约束被违反后,上述的表达式便触发一个编译错误。具有挑战的地方是要找到正确的编译时表达式,在约束被违反时激活这些表达式。此时熟悉
C++ 标准当然是有益而无害的。标准中说非 POD 对象不能是一个联合(union)的成员(见标准的 clause
9.5)。利用这个限制,创建一个联合,让其唯一成员就是你要测试的对象不就行了。
template <class T> struct POD_test
{
POD_test()
{
union
{
T t; //T 必须是一个 POD 类型
} u;
}
};
编译器只为实际被调用的成员函数产生代码,或者显式或者隐式。因此,在某个类模板的构造函数或吸构函数中实现该约束将保证其编译时能处理其每个实例。(稍后我们将看到如何改进此设计)
为了测试这段代码,你可以使用各种不同的模版参数来进行实例化:
//下列三条语句通过编译
POD_test <int> pi;
POD_test <S1> ps1;
POD_test <S4> ps4;
//编译失败
POD_test <std::string> pstr;
POD_test <C1> pc1;
POD_test <C2> pc2;
正像我们期望的那样,由于后面三个实例其模板参数不是 POD 对象,编译器在处理时发出了出错信息。
改进设计
约束能导致运行时和空间上的开销吗?如果它在编译时检查,你肯定不想让它呆在可执行文件中,现代 IDEs 都很聪明,
将可执行文件中不必要的代码优化掉。为了让编译器报错,将约束移到一个单独的静态成员函数 constraints()
中(你也可以另外的函数名)。记住声明时时这个成员是 private,以便其它程序无法调用它:
template <class T> struct POD_test
{
POD_test(){constraints();} // 强制编译时处理
private:
static void constraints()
{
union{ T t;} u;
}
};
注意 constraints()
函数实际上什么也没做,它只是声明了一个局部联合类型的变量。由于该联合变量没有被使用,编译器可以省略掉这个构造函数中的
constraints(),从而避免了必要的开销。
“必须是 POD ” 只是众多强制约束中一个案例。另一个常用的约束是“必须是 T”,实现这个约束也非常简单:
//检查 T1 是否为 T2
template <class T1, class T2> struct is_a_T2
{
is_a_T2() {constraints();}
static void constraints()
{
T1 t1;
T2& ref=t1; // 如果 t1 不是 t2 则出错
}
};
这里是另外一个约束:“必须是一个整型”,这个也不难,有多种技术途径来实现。其中之一就是是使用该对象作为某个数组的下标。因为 C++
需要整型作为下标,使用任何其它的类型都将导致编译错误。我将这个实现留给读者来做。
|