整型常量表达式的编码指南
整型常量表达式在 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),最好是将整型常量表达式的计算移到单独的 traits 类中。
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 的实现方式可能会导致其静态常量成员无法用作整型常量表达式。这与标准相矛盾,但似乎是一个至少影响两个标准库供应商的错误;如果发生这种情况,Boost 会在 <boost/config.hpp> 中定义 BOOST_NO_LIMITS_COMPILE_TIME_CONSTANTS。
- VC6 中存在一个奇怪的错误,其中 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 的编译器进行编译(具体取决于使用的模板参数)。