整型常量表达式编码指南
整型常量表达式在 C++ 中被广泛使用,例如数组边界、位域长度、枚举器初始化器以及非类型模板参数。然而,许多编译器在处理整型常量表达式时存在问题。因此,使用非类型模板参数编程特别困难,经常导致错误地认为非类型模板参数不受特定编译器支持。本文旨在提供一套指南和解决方法,遵循这些指南可以使整型常量表达式在当前所有受 boost 支持的编译器上实现可移植使用。虽然本文主要面向 boost 库作者,但也可能对想要了解 boost 代码编写方式的用户或想要编写可移植代码的用户有所帮助。
什么是整型常量表达式?
整型常量表达式在标准的 5.19 节中描述,有时也称为“编译时常量”。整型常量表达式可以是以下之一:
- 字面量整型值,例如
0u
或3L
。 - 枚举器值。
- 全局整型常量,例如
const int my_INTEGRAL_CONSTANT = 3;
- 静态成员常量,例如
struct myclass { static const int value = 0; };
- 成员枚举器值,例如
struct myclass { enum{ value = 0 }; };
- 整型或枚举类型的非类型模板参数。
sizeof
表达式的结果,例如sizeof(foo(a, b, c))
static_cast
的结果,其中目标类型是整型或枚举类型,参数是另一个整型常量表达式或浮点数字面量。- 将二元运算符应用于两个整型常量表达式
INTEGRAL_CONSTANT1 op INTEGRAL_CONSTANT2
前提是运算符不是赋值运算符或逗号运算符。 - 将一元运算符应用于整型常量表达式
op INTEGRAL_CONSTANT1
前提是运算符不是自增或自减运算符。
编码指南
以下指南按无特定顺序排列(也就是说,你需要遵守所有指南 - 抱歉!),并且可能并不完整,随着编译器发生变化和/或出现更多问题,可能会添加更多指南。
在声明作为类成员的常量时,始终使用宏 BOOST_STATIC_CONSTANT
。
template <class T> struct myclass { BOOST_STATIC_CONSTANT(int, value = sizeof(T)); };
理由:并非所有编译器都支持成员常量的内联初始化,有些编译器对成员枚举器处理方式不同(它们并不总是被视为整型常量表达式)。BOOST_STATIC_CONSTANT
宏针对不同的编译器使用最合适的方法。
不要声明类型比 int 更宽的整型常量表达式。
理由:虽然理论上所有整型类型都可以在整型常量表达式中使用,但在实践中,许多编译器将整型常量表达式限制为不比 int
更宽的类型。
不要在整型常量表达式中使用逻辑运算符;而是使用模板元编程。
头文件 <boost/type_traits/ice.hpp>
包含一些解决方法模板,它们可以实现逻辑运算符的功能,例如,不要使用
INTEGRAL_CONSTANT1 || INTEGRAL_CONSTANT2
而是使用
::boost::type_traits::ice_or<INTEGRAL_CONSTANT1,INTEGRAL_CONSTANT2>::value
理由:一些编译器(特别是 Borland 和 Microsoft 编译器)往往不将涉及逻辑运算符的整型常量表达式识别为真正的整型常量表达式。这个问题通常只在整型常量表达式嵌套在模板代码深层时才会出现,并且难以重现和诊断。
不要在用作非类型模板参数的整型常量表达式中使用任何运算符
不要使用
typedef myclass<INTEGRAL_CONSTANT1 == INTEGRAL_CONSTANT2> mytypedef;
而是使用
typedef myclass< some_symbol> mytypedef;
其中 some_symbol
是一个整型常量表达式的符号名称,其值为 (INTEGRAL_CONSTANT1 == INTEGRAL_CONSTANT2)
。
理由:旧的基于 EDG 的编译器(其中一些用于该平台编译器的最新版本)不识别包含运算符的表达式作为非类型模板参数,即使这些表达式可以在其他地方用作整型常量表达式。
始终使用完全限定名称来引用整型常量表达式。
例如
typedef
myclass< ::boost::is_integral<some_type>::value> mytypedef;
理由:至少一个编译器(Borland 的)在常量名称没有完全限定的情况下不识别它作为整型常量表达式(也就是说,它必须以 ::
开头)。
在 '<
' 之后和 '::
' 之前始终留一个空格。
例如
typedef myclass< ::boost::is_integral<some_type>::value> mytypedef; ^ ensure there is space here!
理由:<:
本身就是一个合法的二合字符,因此 <::
被解释为与 [:
相同。
不要使用局部名称作为整型常量表达式
示例
template <class T> struct foobar { BOOST_STATIC_CONSTANT(int, temp = computed_value); typedef myclass<temp> mytypedef; // error };
理由:至少一个编译器(Borland 的)不支持此操作。
虽然可以通过使用以下方法解决此问题:
template <class T> struct foobar { BOOST_STATIC_CONSTANT(int, temp = computed_value); typedef foobar self_type; typedef myclass<(self_type::temp)> mytypedef; // OK };
但这会破坏至少一个其他编译器(VC6),因此最好将整型常量表达式计算移到一个单独的特征类中
template <class T> struct foobar_helper { BOOST_STATIC_CONSTANT(int, value = computed_value); }; template <class T> struct foobar { typedef myclass< ::foobar_helper<T>::value> mytypedef; // OK };
不要为非类型模板参数使用依赖默认参数。
例如
template <class T, int I = ::boost::is_integral<T>::value> // Error can't deduce value of I in some cases. struct foobar;
理由:这种用法对 Borland C++ 来说会失败。请注意,这仅在默认值依赖于先前的模板参数时才会出现问题,例如,以下操作是可以的
template <class T, int I = 3> // OK, default value is not dependent struct foobar;
未解决的问题
以下问题要么未解决,要么其修复方法特定于编译器,或者会破坏一个或多个编码指南。
小心使用 numeric_limits
这里有三个问题:
- 头文件
<limits>
可能不存在 - 建议不要直接包含<limits>
,而是使用<boost/pending/limits.hpp>
。如果可用,此头文件会包含“真正的”<limits>
头文件,否则它会提供自己的std::numeric_limits
定义。如果<limits>
不存在,Boost 还会定义宏BOOST_NO_LIMITS
。 std::numeric_limits
的实现方式可能导致其静态常量成员无法用作整型常量表达式。这与标准相矛盾,但似乎是一个 bug,影响了至少两个标准库供应商;如果发生这种情况,Boost 会在<boost/config.hpp>
中定义BOOST_NO_LIMITS_COMPILE_TIME_CONSTANTS
。- VC6 中存在一个奇怪的 bug,在模板代码中,
std::numeric_limits
的成员可能会“过早地求值”,例如
template <class T> struct limits_test { BOOST_STATIC_ASSERT(::std::numeric_limits<T>::is_specialized); };
即使从未创建过模板实例,此代码在 VC6 中也无法编译;由于某种奇怪的原因,::std::numeric_limits<T>::is_specialized
始终返回 false,无论模板参数 T 是什么。问题似乎局限于依赖 std::numeric_limts
的表达式:例如,如果将 ::std::numeric_limits<T>::is_specialized
替换为 ::boost::is_arithmetic<T>::value
,一切正常。以下解决方法也有效,但与编码指南冲突
template <class T> struct limits_test { BOOST_STATIC_CONSTANT(bool, check = ::std::numeric_limits<T>::is_specialized); BOOST_STATIC_ASSERT(check); };
因此,最好使用类似以下的方法
template <class T> struct limits_test { #ifdef BOOST_MSVC BOOST_STATIC_CONSTANT(bool, check = ::std::numeric_limits<T>::is_specialized); BOOST_STATIC_ASSERT(check); #else BOOST_STATIC_ASSERT(::std::numeric_limits<T>::is_specialized); #endif };
小心使用 sizeof 运算符
据我所知,所有编译器在参数是类型名称(或模板 ID)时都能正确处理 sizeof
表达式,但如果出现以下情况,可能会出现问题:
- 参数是成员变量的名称或局部变量的名称(代码在 VC6 中可能无法编译)。
- 参数是一个表达式,该表达式涉及创建临时对象(代码在 Borland C++ 中将无法编译)。
- 参数是一个表达式,其中包含重载函数调用(代码可以编译,但结果在 Metroworks C++ 中是垃圾值)。
不要使用 boost::is_convertible,除非必须
由于 is_convertible
是根据 sizeof
运算符实现的,因此在 Metroworks 编译器中始终返回错误的值,并且在 Borland 的编译器中可能无法编译(取决于使用的模板参数)。