整數常數表達式的編碼準則
整數常數表達式在 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 的實作可能會以其靜態常數成員無法用作整數常數表達式的方式定義。這與標準衝突,但似乎是至少影響兩個標準函式庫供應商的錯誤;在此情況下,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 編譯;由於某些奇怪的原因,無論樣板參數 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 運算子
據我所知,當引數是類型名稱(或樣板識別碼)時,所有編譯器都會正確處理 sizeof 表達式,但是如果發生以下情況,則可能會發生問題
- 引數是成員變數或區域變數的名稱(程式碼可能無法使用 VC6 編譯)。
- 引數是一個涉及建立暫時變數的表達式(程式碼無法使用 Borland C++ 編譯)。
- 引數是一個涉及多載函式呼叫的表達式(程式碼會編譯,但結果會是 Metroworks C++ 中的垃圾值)。
除非必要,否則不要使用 boost::is_convertible
由於 is_convertible 是根據 sizeof 運算子實作的,因此當與 Metroworks 編譯器一起使用時,它會始終給出錯誤的值,並且可能無法使用 Borland 的編譯器編譯(取決於使用的樣板引數)。