整型常量表达式的编码指南
整型常量表达式在 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 的实现可能以某种方式定义,使其静态常量成员可能无法用作整型常量表达式。这与标准相矛盾,但似乎是一个影响至少两个标准库供应商的 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 编译;由于某些奇怪的原因,无论模板参数 T 是什么,::std::numeric_limits<T>::is_specialized
始终求值为 false。问题似乎仅限于依赖于 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 编译器编译(取决于所使用的模板参数)。