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
的定义. 这样如果expr
为true
, 那就会创建一个空的结构体, 无事发生. 如果expr
为false
, 那么就会尝试创建一个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
的构造函数中. 如果expr
为true
, 那这个特化版本的构造函数可以接收任何参数, 传入一个ERROR_##msg
对象能够通过编译. 如果expr
为false
, 那这个特化版本的构造函数不接受任何参数, 传入一个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++中宏基本都用在各种日志库/编译期检查.