C++11标准新引入的static_assert功能可以实现静态断言,是一个非常强大的模板元编程工具,配合SFINAE特性可以在编译期发现不符合预期的不合理特化,并且给出自定义的错误信息。
但是不同编译器的实现却不尽相同,于是招致了一些不必要的麻烦。
g++为了效率,在某些情况下会过早、甚至不必要地执行了static_assert断言,导致编译失败。本文给出了这种问题的发出条件与解决方法。
static_assert的两个用例
用例1:
使用单一泛型实现,直接在static_assert上使用模板元。
编译时直接拒绝不符合要求的类型,并为其他的类型提供统一的泛型实现,即无法为不同类型提供不同的实现。
特点:
实现简单。适用于仅接受某一版本的情况。
g++和VC都支持且表现一致。
例如:
#include <type_traits>
template <class T>
T func(const T& t){
static_assert(!std::is_integer<T>::value, "Type T should be an interger, like: int, short, long.");
return 12345 % t;
}
用例2:
使用偏特化提供不同版本的实现,并用static_assert(false)拒绝其他类型。
特点:
实现较为复杂,可以提供多种不同实现。
VC支持,而g++不支持。
例如:
(C++17及之前标准不支持模板函数的偏特化,这是使用一个模板类和一个代理函数。)
#include <type_traits> // is_integer, is_floating_point, enable_if
#include <cmath> // fmod
template <class T, class Enable = void>
class A{
T func(const T& t){
static_assert(false, "Do not support this type.");
return T();
}
};
// 整型版本
template <class T>
class A<T, typename std::enable_if<std::is_integer<T>::value>::type>{
T func(const T& t){
return 12345 % t;
}
};
// 浮点版本
template <class T>
class A<T, typename std::enable_if<std::is_floating_point<T>::value>::type>{
T func(const T& t){
return std::fmod(T(12345), t);
}
};
// 代理函数
template <class T>
T func(const T& t){
A<T> temp;
return temp.func(t);
}
基本原理:
- 模板函数只有在被特化后,相应代码才会生成,此时相关逻辑才会被检查;
- 使用偏特化(partial specialization)实现特定功能;
- 将static_assert(false,”message”)放在不应该存在的特化类型里面。
在这个例子里面,我们在不指定类型的、任何类型都可以匹配的那个方法里面写了一个一旦被执行必然报错的语句:static_assert(false,”…”)。
按照正常的函数决意流程(具体函数->隐式类型转换后匹配的具体函数->偏特化的模板函数->模板函数),如果某个类型无法匹配已经提供的偏特化版本,它将被最一般的那个版本所接收,此时static_assert(false,”…”)语句讲被编译为具体函数。在编译器执行到这一条语句的时候,将会触发一个编译错误,提示程序员:”Do not support this type. “。
这里的例子比较简单,即使不用static_assert,在编译时也可以因为%
或fmod
的操作数不匹配而报错,但是在一些更复杂的情况(如序列化之类涉及指针的操作)下,编译时无法发现问题,只会在运行时出错。
以上例子在VS 2015上编译通过,并且工作正常。
g++上的问题
g++无论是否有将某个包含static_assert(false)的函数特化,都会在编译该文件时执行static_assert(false)函数,直接导致编译失败。
于是我们不能直接使用外置模板元模块,并且提供一般化版本的方法来实现这种模板级别的“多态”。
根据我的测试:此问题至少存在于GCC 4.9, 5.4, 6.3版本,并且高度怀疑存在于目前所有的支持C++11的GCC版本中。
g++上的解决方案
原因分析
按照最基本的情况:static_assert的条件变量,是通过一个编译期可求值的模板表达式来提供的,而不是直接提供一般具体的bool常量。
例如:
std::is_integer<int>::value
static_assert(std::is_integer<T>::value)
同时形如std::false_type::value
这样的非模板表达式也是不行的,会被直接执行。
所以认为:
- 在g++的处理过程中,只有当static_assert语句本身包含了模板特化过程,它才根据需要作出执行还是不执行的决定,而不管static_assert所在的模块是否被特化了。
也就是说static_assert的执行与否如果可以在语法检查阶段完成,就绝不拖到模板特化时(略早于中间代码生成)再进行,从而提高效率。 - 而VS的cl编译器在处理的过程中,则很有可能是在中间代码生成的阶段(模板特化之后)进行针对static_assert的判断。
解决方法
所以g++接受的做法是我们将这个值包裹在一个模板表达式里面再提供给它,试static_assert语句中包含模板特化。
具体技巧也很简单,我们可以自己写一个模板化的false_type。
代码如下:
// Templated False Type:
template<class T>
struct TFT : public std::false_type {};
// 原来的代码替换为:
template <class T, class Enable = void>
struct Serializer {
char* serial(char* res, int bufSize, const T& item) {
static_assert(TFT<T>::value, "The serialization of this type is not provided.");
return nullptr;
}
};
本代码在GCC 4.9和5.4上测试通过。