簡體   English   中英

為什么模板參數替換的順序很重要?

[英]Why does the order of template argument substitution matter?

C ++ 11

14.8.2 - 模板參數演繹 - [temp.deduct]

7替換發生在函數類型和模板參數聲明中使用的所有類型和表達式中。 表達式不僅包括常量表達式,例如出現在數組邊界中的常量表達式,還包括非類型模板參數,還包括sizeofdecltype和允許非常量表達式的其他上下文中的通用表達式(即非常量表達式)。


C ++ 14

14.8.2 - 模板參數演繹 - [temp.deduct]

7替換發生在函數類型和模板參數聲明中使用的所有類型和表達式中。 表達式不僅包括常量表達式,例如出現在數組邊界中的常量表達式,還包括非類型模板參數,還包括sizeofdecltype和允許非常量表達式的其他上下文中的通用表達式(即非常量表達式)。 替換以詞匯順序進行,並在遇到導致演繹失敗的條件時停止



添加的句子明確說明了在C ++ 14中處理模板參數時的替換順序。

替換順序通常不會引起很多關注。 我還沒有找到一篇關於其重要性的論文。 也許這是因為C ++ 1y尚未完全標准化,但我認為必須引入這樣的改變是有原因的。

問題:

  • 為什么以及何時,模板參數替換的順序是否重要?

如上所述,C ++ 14明確指出模板參數替換的順序是明確定義的; 更具體地說,它將保證以“詞匯順序”進行,並在替換導致扣除失敗時停止。

與C ++ 11相比,在C ++ 14中編寫SFINAE代碼(包含一個依賴於另一個規則的代碼)會更容易,我們也將遠離模板替換的未定義排序可能使我們的整個應用程序受到影響的情況。未定義行為。

注意 :重要的是要注意C ++ 14中描述的行為一直是預期的行為,即使在C ++ 11中,只是它沒有以這種明確的方式措辭。



這種變化背后的理由是什么?

這一變化背后的原因可以在DanielKrügler最初提交的缺陷報告中 找到


進一步解釋

在編寫SFINAE時,我們作為開發人員依賴於編譯器來查找在使用時在我們的模板中產生無效類型表達式的任何替換。 如果找到這樣的無效實體,我們無視模板宣告的內容,繼續尋找合適的匹配。

替換失敗不是一個錯誤 ,但僅僅是...... “噢,這不起作用..請繼續前進”

問題是只能在替換的直接上下文中查找潛在的無效類型和表達式。

14.8.2 - 模板參數演繹 - [temp.deduct]

8如果替換導致無效的類型或表達式,則類型推導失敗。 如果使用替換參數寫入,則無效的類型或表達式將是格式錯誤的。

[ 注意:訪問檢查是作為替換過程的一部分完成的。 - 后注 ]

只有函數類型的直接上下文中的無效類型和表達式及其模板參數類型才會導致演繹失敗。

[ 注意:對替換類型和表達式的評估可能會導致副作用,例如類模板特化和/或函數模板特化的實例化,隱式定義函數的生成等。這些副作用不在“立即上下文“並且可能導致程序格式不正確。 - 后注 ]

換句話說,在非直接上下文中發生的替換仍然會使程序形成錯誤,這就是模板替換的順序很重要的原因; 它可以改變某個模板的全部含義。

更具體地說,它可以是具有一個模板在SFINAE可用的,和一個模板這是不之間的差。


SILLY例子

template<typename SomeType>
struct inner_type { typedef typename SomeType::type type; };

template<
  class T,
  class   = typename T::type,            // (E)
  class U = typename inner_type<T>::type // (F)
> void foo (int);                        // preferred

template<class> void foo (...);          // fallback

struct A {                 };  
struct B { using type = A; };

int main () {
  foo<A> (0); // (G), should call "fallback "
  foo<B> (0); // (H), should call "preferred"
}

在標記為(G)的行上,我們希望編譯器首先檢查(E) ,如果成功評估(F) ,但在本文中討論的標准更改之前,則沒有這樣的保證。


foo(int)替換的直接上下文包括:

  • (E)確保傳入的T具有::type
  • (F)確保inner_type<T>具有::type


如果(F)被評估,即使(E)導致無效替換,或者如果(F)(E)之前評估,我們的短(愚蠢)例子將不會使用SFINAE,我們將得到診斷說我們的應用程序是不正確的..即使我們打算在這種情況下使用foo(...)


注意:請注意, SomeType::type不在模板的直接上下文中; inner_type中的typedef inner_type將導致應用程序inner_type並阻止模板使用SFINAE



這會對C ++ 14中的代碼開發產生什么影響?

這種變化將極大地簡化語言律師的生活,他們試圖實現一些保證以某種方式(和順序)進行評估的東西,無論他們使用什么符合標准的編譯器。

它還將使模板參數替換以更自然的方式表現為非語言律師 ; 從左到右進行替換遠比erhm-like-way-to-compiler-wanna-do-like-like-erhm -...更直觀。


是否有任何負面含義?

我唯一能想到的是,由於替換順序將從左到右發生,因此不允許編譯器使用異步實現一次處理多個替換。

我還沒有偶然發現這樣的實現,我懷疑它會導致任何重大的性能提升,但至少理論上的想法有點適合於事物的“消極”方面。

作為一個例子:編譯器將無法使用兩個同時進行替換的線程,在沒有任何機制的情況下,在沒有任何機制的情況下執行替換,就像在某個點之后發生的替換一樣,如果需要的話。



故事

注意 :本節將介紹可以從現實生活中獲取的示例,以描述模板參數替換的順序何時以及為何重要。 如果有任何不夠清楚,甚至可能是錯誤的,請告訴我(使用評論部分)。

想象一下,我們正在使用枚舉器 ,並且我們想要一種方法來輕松獲取指定枚舉基礎

基本上我們厭倦了總是不得不寫(A) ,當我們理想地想要更接近(B)東西時。

auto value = static_cast<std::underlying_type<EnumType>::type> (SOME_ENUM_VALUE); // (A)

auto value = underlying_value (SOME_ENUM_VALUE);                                  // (B)

原始實施

說和做,我們決定寫的實現underlying_value地看着下面。

template<class T, class U = typename std::underlying_type<T>::type> 
U underlying_value (T enum_value) { return static_cast<U> (enum_value); }

這將緩解我們的痛苦,似乎完全符合我們的要求; 我們傳入一個枚舉器,並獲取基礎值。

我們告訴自己,這個實施很棒,並要求我們的同事( 堂吉訶德 )坐下來審查我們的實施,然后再將其推向生產階段。


代碼審查

Don Quixote是一位經驗豐富的C ++開發人員,一手拿着咖啡,另一手拿着C ++標准。 如何用雙手忙着編寫一行代碼是一個謎,但這是一個不同的故事。

他回顧了我們的代碼並得出結論,實現是不安全的,我們需要保護std::underlying_type免於未定義的行為,因為我們可以傳入一個不是枚舉類型T

20.10.7.6 - 其他轉換 - [meta.trans.other]

 template<class T> struct underlying_type; 

條件: T應為枚舉類型(7.2)
注釋:成員typedef type應命名T的基礎類型。

注意:標准指定了underlying_type條件 ,但它沒有進一步說明如果用非枚舉實例化會發生什么。 由於我們不知道在這種情況下會發生什么,因此使用屬於未定義的行為 ; 它可能是純UB ,使應用程序形成不良,或在線訂購食用內衣。


閃電盔甲的騎士

Don大吼大叫我們應該如何始終尊重C ++標准,我們應該為我們所做的事感到極大的恥辱......這是不可接受的。

在他平靜下來並喝了幾口咖啡之后,他建議我們改變實現以增加保護,防止用不允許的東西實例化std::underlying_type

template<
  typename T,
  typename   = typename std::enable_if<std::is_enum<T>::value>::type,  // (C)
  typename U = typename std::underlying_type<T>::type                  // (D)
>
U underlying_value (T value) { return static_cast<U> (value); }

風車

我們感謝Don的發現並且現在對我們的實現感到滿意,但直到我們意識到模板參數替換的順序在C ++ 11中沒有明確定義(當替換將停止時也沒有說明)。

編譯為C ++ 11,我們的實現仍然可以導致std::underlying_type的實例化,其中T不是枚舉類型,原因有兩個:

  1. 編譯器可以在(C) (D)之前自由地評估(D) ,因為替換順序沒有明確定義,並且;

  2. 即使編譯器在(D) (C)之前評估(C) ,也不保證它不會評估(D) ,C ++ 11沒有明確說明替換鏈何時必須停止的子句。


Don的實現在C ++ 14中沒有未定義的行為 ,但僅僅因為C ++ 14明確指出替換將以詞法順序進行 ,並且只要替換導致演繹失敗 ,它就會停止

唐可能不會在這個風車上打風,但他肯定錯過了C ++ 11標准中非常重要的龍。

C ++ 11中的有效實現需要確保無論模板參數替換發生的順序如何, std::underlying_type的瞬時都不會出現無效類型。

#include <type_traits>

namespace impl {
  template<bool B, typename T>
  struct underlying_type { };

  template<typename T>
  struct underlying_type<true, T>
    : std::underlying_type<T>
  { };
}

template<typename T>
struct underlying_type_if_enum
  : impl::underlying_type<std::is_enum<T>::value, T>
{ };

template<typename T, typename U = typename underlying_type_if_enum<T>::type>
U get_underlying_value (T value) {
  return static_cast<U> (value);  
}

注意:使用了 underlying_type ,因為它是一種簡單的方法,可以在標准中使用標准中的內容; 重要的是用非枚舉實例化它是未定義的行為

之前在本文中鏈接的缺陷報告使用了一個更為復雜的例子,該例子假設有關此事的廣泛知識。 我希望這個故事對於那些沒有很好地閱讀這個主題的人來說是一個更合適的解釋。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM