C++11以前

在C++11之前, 如果想检查某个条件, 可以通过下面assert进行断言

template <class To, class From>
To safe_reinterpret_cast(From from) {
	assert(sizeof(To) >= sizeof(From)); // 转换不能缩小类型
	return reinterpret_cast<To>(from);
}

但问题是assert是一个运行期断言, 它只有在运行期才会执行检查, 而且在Release构建下还会被编译器优化掉. 那如果想在编译期就做检查并发现潜在的错误呢? 可以用下面的这个方法

#define STATIC_CHECK(expr) \
	{ char unnamed[expr ? 1 : 0]; }

template <class To, class From>
To safe_reinterpret_cast(From from) {
	STATIC_CHECK(sizeof(To) >= sizeof(From)); // 转换不能缩小类型
	return reinterpret_cast<To>(from);
}

这样如果在编译期对safe_reinterpret_cast进行特化的时候发现条件不满足, 就会尝试创建一个长度为0的数组, 这就会引发编译器报错了. 但问题是这种报错非常难以理解, 创建一个长度为0的数组和类型转换错误有什么关系? 所以必须再找一个别的方法, 比如通过模板特化

template <bool> struct CompileTimeAssert;
template <> struct CompileTimeAssert<true> {};

#define STATIC_CHECK(expr) \
	(CompileTimeAssert<(expr != 0)>());

template <class To, class From>
To safe_reinterpret_cast(From from) {
	STATIC_CHECK(sizeof(To) >= sizeof(From)); // 转换不能缩小类型
	return reinterpret_cast<To>(from);
}

首先声明了CompileTimeAssert这个结构体, 但并未给出它的定义. 接着利用模板全特化, 给出了当模板参数为true的时候, CompileTimeAssert的定义. 这样如果exprtrue, 那就会创建一个空的结构体, 无事发生. 如果exprfalse, 那么就会尝试创建一个CompileTimeAssert<false>的结构体, 但这个结构体并未定义, 因此引发编译器报错, 报错内容类似于Undefined specialization of 'CompileTimeAssert<false>'. 这样似乎能获得好一些的编译期报错, 至少我们能知道是因为某个检查出现错误导致的问题.

有没有办法做的更好呢? 看下面这个版本, 它依然是基于模板全特化的

template <bool> struct CompileTimeAssert {
	CompileTimeAssert(...);
};
template <> struct CompileTimeAssert<false> {};

#define STATIC_CHECK(expr, msg) \
	{\
		class ERROR_##msg {}; \
		(void)sizeof(CompileTimeAssert<(expr != 0)>(ERROR_##msg())); \
	}

template <class To, class From>
To safe_reinterpret_cast(From from) {
	STATIC_CHECK(sizeof(To) >= sizeof(From), Destination_Type_Too_Narrow); // 转换不能缩小类型
	return reinterpret_cast<To>(from);
}

开始变得复杂起来了, 我们逐步分解一下. 首先定义了标准的CompileTimeAssert, 它的构造函数可以接受任何参数. 接着定义了CompileTimeAssert<false>这个特化版本. 它只有编译器自动生成的无参构造函数.

接着定义STATIC_CHECK宏. 这个宏会先定义一个局部类, 类名由ERROR_前缀和给定的宏参数msg构成. 接着来看最复杂的一部分, 它首先创建一个ERROR_##msg对象, 这个对象被传入到CompileTimeAssert的构造函数中. 如果exprtrue, 那这个特化版本的构造函数可以接收任何参数, 传入一个ERROR_##msg对象能够通过编译. 如果exprfalse, 那这个特化版本的构造函数不接受任何参数, 传入一个ERROR_##msg对象就会引发编译器报错, 报错内容类似于Cannot find a conversion from ERROR_Destination_Type_Too_Narrow to CompileTimeAssert<false>.

现在这个报错的可读性就已经不错了, 如果加上__LINE__宏和__FILE__宏, 那报错提示效果就会进一步提升. 另外需要注意的是上面的代码使用了(void)sizeof, sizeof本身是用来获取表达式或者类型的大小, 这里用它来强制编译器在编译时评估其中表达式的有效性. 而(void)则是防编译器警告, 因为sizeof本身会返回一个值, 而这个值我们并未赋给任何一个变量.

总结

总之我们利用模板特化的技巧实现了一个编译期检查的宏, 这在C++11之前是一个常见的trick, 而在C++11引入static_assert后, 可以直接用static_assert做编译期检查, 获得更好的提示.

这是一个简单的模板元编程示例, 对于初学者而且可能有些混乱, 或者也有可能会问, 哎呀, 怎么不用printf, 不用cout打印出报错信息msg呢? 因为这是编译期检查, 而printf, cout这些都是运行时函数, 它们在编译期没有任何作用, 在运行时才会打印出报错信息.

另外学校中的某些课本上提到应该尽量少用#define宏, 因为没有类型检查. 自然而然会有初学者觉得, 哎呀, C/C++宏缺点这么明显怎么还不弃用, 除了做"Include Guard"还有什么地方必须用? 从上面这个用法就可以看出来, 如果没有宏, 稍微复杂一点的编译期检查还真写不出来. 的确, 大部分地方必须用宏的确实不多, 现代C++中宏基本都用在各种日志库/编译期检查.

本页内容