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

标签 c++ templates c++11 language-lawyer c++14

C++11

14.8.2 - Template Argument Deduction - [temp.deduct]

7 The substitution occurs in all types and expressions that are used in the function type and in template parameter declarations. The expressions include not only constant expressions such as those that appear in array bounds or as nontype template arguments but also general expressions (ie. non-constant expressions) inside sizeof, decltype, and other contexts that allow non-constant expressions.




C++14

14.8.2 - Template Argument Deduction - [temp.deduct]

7 The substitution occurs in all types and expressions that are used in the function type and in template parameter declarations. The expressions include not only constant expressions such as those that appear in array bounds or as nontype template arguments but also general expressions (ie. non-constant expressions) inside sizeof, decltype, and other contexts that allow non-constant expressions. The substitution proceeds in lexical order and stops when a condition that causes deduction to fail is encountered.





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

替换顺序是最常不被重视的事情。我还没有找到一篇关于为什么这很重要的论文。也许这是因为 C++1y 还没有完全标准化,但我认为引入这样的改变肯定是有原因的。

问题:
  • 为什么以及何时模板参数替换的顺序很重要?
  • 最佳答案

    如前所述,C++14 明确表示模板参数替换的顺序是明确定义的;更具体地说,它将保证按“词汇顺序进行并在替换导致演绎失败时停止。

    与 C++11 相比,在 C++14 中编写包含一个规则依赖于另一个规则的 SFINAE 代码要容易得多,我们还将摆脱模板替换的未定义顺序可能使我们的整个应用程序受到影响的情况未定义的行为。

    Note :需要注意的是,C++14 中描述的行为一直是预期的行为,即使在 C++11 中也是如此,只是它没有以如此明确的方式措辞。



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

    此更改背后的原始原因可以在 Daniel Krügler 最初提交的缺陷报告中找到:

  • C++ Standard Core Language Defect Reports and Accepted Issues, Revision 88
  • 1227. Mixing immediate and non-immediate contexts in deduction failure


  • 进一步说明

    在编写 SFINAE 时,我们作为开发人员依赖于编译器来查找在使用时会在我们的模板中产生无效类型或表达式的任何替换。如果发现这样的无效实体,我们希望忽略模板声明的任何内容,并继续希望找到合适的匹配项。

    替换失败不是错误,而只是..“啊,这不起作用..请继续”。

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

    14.8.2 - Template Argument Deduction - [temp.deduct]

    8 If a substitution results in an invalid type or expression, type deduction fails. An invalid type or expression is one that would be ill-formed if written using the substituted arguments.

    [ Note: Access checking is done as part of the substitution process. --end note ]

    Only invalid types and expressions in the immediate context of the function type and its template parameter types can result in a deduction failure.

    [ Note: The evaluation of the substituted types and expressions can result in side effects such as the instantiation of class template specializations and/or function template specializations, the generation of implicitly-defined functions, etc. Such side effects are not in the "immediate context" and can result in the program being ill-formed. --end note]



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

    更具体地说,这可能是具有 的模板与可用于 SFINAE 的模板和 不是 0x2518194123411 的模板之间的区别

    愚蠢的例子
    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) 之前被评估,我们的简短(愚蠢的)应用程序将被错误诊断,我们的应用程序将不会被使用。虽然我们打算在这种情况下使用 foo(...)

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



    这对 C++14 中的代码开发有什么影响?

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

    它还将使模板参数替换以更自然的方式对非语言律师进行操作;从左到右进行替换比 erhm-like-any-way-the-compiler-wanna-do-it-like-erhm-....

    没有任何负面影响吗?

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

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

    例如:如果需要,编译器将无法在实例化某个模板时使用同时执行替换的两个线程,而没有任何机制来像在某个点之后发生的替换从未发生过一样。



    故事

    注释 :本节将提供一个可能取自现实生活的示例,以描述模板参数替换的顺序何时以及为何如此重要。如果有什么不够清楚,甚至可能是错误的,请告诉我(使用评论部分)。

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

    基本上我们厌倦了总是不得不写 (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); }
    

    这将减轻我们的痛苦,并且似乎正是我们想要的;我们传入一个枚举器,并取回基础值。

    我们告诉自己这个实现很棒,并请我们的一位同事(唐吉诃德)坐下来审查我们的实现,然后再将其投入生产。

    代码审查

    唐吉诃德是一位经验丰富的 C++ 开发人员,他一手拿着一杯咖啡,另一只手拿着 C++ 标准。他是如何在双手忙碌的情况下设法编写一行代码的,这是一个谜,但这是另一回事。

    他审查了我们的代码并得出结论,该实现是不安全的,我们需要保护 std::underlying_type 免受未定义行为的影响,因为我们可以传入一个 T 不是枚举类型。

    20.10.7.6 - Other Transformations - [meta.trans.other]

    template<class T> struct underlying_type;
    

    Condition: T shall be an enumeration type (7.2)
    Comments: The member typedef type shall name the underlying type of T.



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

    披着 Shiny 盔甲的骑士

    唐对我们应该如何始终尊重 C++ 标准大喊大叫,我们应该为我们所做的感到非常羞耻......这是 Not Acceptable 。

    在他冷静下来并再喝几口咖啡后,他建议我们更改实现以增加保护,防止使用不允许的内容实例化 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 不是枚举类型,原因有两个:
  • 编译器可以在 (D) 之前自由评估 (C),因为替换顺序没有明确定义,并且;
  • 即使编译器在 (C) 之前计算了 (D) ,也不能保证它不会计算 (D) 。当 C++1 链没有明确说时,必须停止替换链子句。


  • Don 的实现将不受 C++14 中未定义行为的影响,但这只是因为 C++14 明确声明替换将按词法顺序进行,并且只要替换导致推导失败,它就会停止。

    Don 可能不会在这个问题上与风车作斗争,但他肯定错过了 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 之所以被使用,是因为它是一种使用标准中的东西来对抗标准中的东西的简单方法;重要的一点是用非枚举实例化它是未定义的行为。

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

    关于c++ - 为什么模板参数替换的顺序很重要?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/22368022/

    相关文章:

    C++11 右值引用寻址

    c++ - 为什么不能将模板函数作为模板模板参数传递?

    c++ - 类 X 的配置类,它应该嵌套在 X 中还是 X 之外?

    c++ - 如何将 std::bind 对象传递给函数

    c++ - 如何使用模板将 lambda 转换为 std::function

    c++ - Lotus Notes C++ API - 未指定平台

    c++ - CFileDialog 实例化的问题

    c++ - 我可以使用 Visual Studio 201 0's C++ compiler with Visual Studio 2008' s C++ 运行时库吗?

    c++ - 当类型包含具有给定名称和类型的静态变量时启用_if 函数

    c++ - recursive_invoke_result_t 模板结构