首页 文章

为什么模板参数替换的顺序很重要?

提问于
浏览
56

C++11

14.8.2 - 模板参数扣除 - [temp.deduct] 7替换发生在函数类型和模板参数声明中使用的所有类型和表达式中 . 表达式不仅包括常量表达式,例如出现在数组边界中的常量表达式,还包括非类型模板参数,还包括sizeof,decltype和允许非常量表达式的其他上下文中的通用表达式(即非常量表达式) .


C++14

14.8.2 - 模板参数扣除 - [temp.deduct] 7替换发生在函数类型和模板参数声明中使用的所有类型和表达式中 . 表达式不仅包括常量表达式,例如出现在数组边界中的常量表达式,还包括非类型模板参数,还包括sizeof,decltype和允许非常量表达式的其他上下文中的通用表达式(即非常量表达式) . 替换以词汇顺序进行,并在遇到导致演绎失败的条件时停止 .



添加的句子明确说明了在C 14中处理模板参数时的替换顺序 .

替换顺序通常不会引起很多关注 . 我还没有找到一篇关于其重要性的论文 . 也许这是因为C 1y还没有完全标准化,但我认为必须引入这样的改变是有原因的 .

The question:

  • 为什么以及何时,模板参数替换的顺序是否重要?

1 回答

  • 59

    如上所述,C 14明确表示模板参数替换的顺序是明确定义的;更具体地说,它将保证以“词汇顺序”进行,并在替换导致扣除失败时停止 .

    与C 11相比,在C 14中编写由一个规则组成的SFINAE代码要容易得多,我们也将远离模板替换的未定义排序可能使我们的整个应用程序遭受未定义行为的情况 .

    注意:重要的是要注意C 14中描述的行为一直是预期的行为,即使在C 11中,只是它没有以这种明确的方式措辞 .



    这种变化背后的理由是什么?

    这一变化背后的原因可以在DanielKrügler最初提交的缺陷报告中找到:


    FURTHER EXPLANATION

    在编写SFINAE时,我们作为开发人员依赖于编译器来查找在使用时在我们的模板中产生无效类型或表达式的任何替换 . 如果找到这样的无效实体,我们无视模板宣告的内容,继续寻找合适的匹配 .

    替换失败不是一个错误,但仅仅是.. "aw, this didn't work.. please move on" .

    问题是只能在替换的直接上下文中查找潜在的无效类型和表达式 .

    14.8.2 - 模板参数扣除 - [temp.deduct] 8如果替换导致无效的类型或表达式,则类型推导失败 . 如果使用替换参数写入,则无效的类型或表达式将是格式错误的 . [注意:访问检查是作为替换过程的一部分完成的 . --end note]只有函数类型的直接上下文中的无效类型和表达式及其模板参数类型才会导致演绎失败 . [注意:对替换类型和表达式的评估可能会导致副作用,例如类模板特化和/或函数模板特化的实例化,隐式定义函数的生成等 . 这些副作用不在“立即上下文“并且可能导致程序格式不正确 . - 后注]

    换句话说,在非直接上下文中发生的替换仍然会使程序形成错误,这就是模板替换的顺序很重要的原因;它可以改变某个模板的全部含义 .

    更具体地说,它可以是具有在SFINAE中可用的模板和具有 isn't 的模板之间的区别 .


    SILLY EXAMPLE

    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) 导致无效替换,或者如果在 (E) 之前评估 (F) ,我们的短(愚蠢)示例将不会使用SFINAE,我们将得到诊断说我们的应用程序格式不正确..甚至虽然我们打算在这种情况下使用 foo(...) .

    注意:请注意,SomeType :: type不在模板的直接上下文中; inner_type中的typedef失败将导致应用程序格式错误并阻止模板使用SFINAE .



    这会对C 14中的代码开发产生什么影响?

    这种变化将极大地简化语言律师的生活,他们试图实现一些保证以某种方式(和顺序)进行评估的东西,无论他们使用什么符合标准的编译器 .

    它还将使模板参数替换以更自然的方式表现为非语言律师;从左到右进行替换远比erhm-like-way-to-compiler-wanna-do-it-like-erhm -....更直观


    Isn't there any negative implication?

    我唯一能想到的是,由于替换顺序将从左到右发生,因此不允许编译器使用异步实现一次处理多个替换 .

    我还没有偶然发现这样的实现,我怀疑它会导致任何重大的性能提升,但至少理论上的想法有点适合于事物的“消极”方面 .

    作为一个例子:编译器将无法使用两个同时进行替换的线程,在没有任何机制的情况下,在没有任何机制的情况下执行替换,就像在某个点之后发生的替换一样,如果需要的话 .



    故事

    注意:本节将介绍可以从现实生活中获取的示例,以描述模板参数替换的顺序何时以及为何重要 . 如果有任何不够清楚,甚至可能是错误的,请告诉我(使用评论部分) .

    想象一下,我们正在使用枚举器,并且我们想要一种方法来轻松获取指定 enumerationunderlying 值 .

    基本上我们厌倦了总是不得不写 (A) ,当我们理想地想要更接近 (B) 时 .

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

    THE ORIGINAL IMPLEMENTATION

    说完了,我们决定写一个 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); }
    

    这将缓解我们的痛苦,似乎完全符合我们的要求;我们传入一个枚举器,并获取基础值 .

    我们告诉自己,这个实施很棒,并要求我们的同事(堂吉诃德)坐下来审查我们的实施,然后再将其推向 生产环境 阶段 .


    THE CODE REVIEW

    Don Quixote是一位经验丰富的C开发人员,一手拿着一杯咖啡,另一手拿着C标准 . 它's a mystery how he manages to write a single line of code with both hands busy, but that'是一个不同的故事 .

    他回顾了我们的代码并得出结论,实现是不安全的,我们需要保护 std::underlying_type 来自未定义的行为,因为我们可以传入一个不是枚举类型的 T .

    20.10.7.6 - 其他转换 - [meta.trans.other] template <class T> struct underlying_type;
    条件:T应为枚举类型(7.2)注释:成员typedef类型应命名T的基础类型 .

    注意:标准指定了underlying_type的条件,但它没有进一步说明如果用非枚举实例化会发生什么 . 由于我们不知道在这种情况下会发生什么,因此使用属于未定义的行为;它可能是纯UB,使应用程序形成不良,或在线订购食用内衣 .


    THE KNIGHT IN SHINING ARMOUR

    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); }
    

    THE WINDMILL

    我们感谢Don的发现,现在对我们的实施感到满意,但直到我们意识到这一点模板参数替换的顺序在C 11中没有明确定义(当替换将停止时也没有说明) .

    编译为C 11,我们的实现仍然可以导致 std::underlying_type 实例化 T ,而不是枚举类型,因为有两个原因:

    • 编译器可以在 (C) 之前自由评估 (D) ,因为替换顺序没有明确定义,并且;

    • 即使编译器在 (D) 之前评估 (C) ,它's not guaranteed that it won' t评估 (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,因为它是一种简单的方法,可以在标准中使用标准中的内容;重要的是用非枚举实例化它是未定义的行为 .

    之前在本文中链接的缺陷报告使用了一个更为复杂的例子,该例子假设有关此事的广泛知识 . 我希望这个故事对于那些没有很好地阅读这个主题的人来说是一个更合适的解释 .

相关问题