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

Fil*_*efp 56 c++ templates language-lawyer c++11 c++14

C++ 11

14.8.2 - 模板参数扣除 -[temp.deduct]

7替换发生在函数类型和模板参数声明中使用的所有类型和表达式中.表达式不仅包括常量表达式如那些出现在数组边界或无类型模板参数而且一般表达式(即非常量表达式)的内部sizeof,decltype和其它上下文允许非常量表达式.


C++ 14

14.8.2 - 模板参数扣除 -[temp.deduct]

7替换发生在函数类型和模板参数声明中使用的所有类型和表达式中.表达式不仅包括常量表达式如那些出现在数组边界或无类型模板参数而且一般表达式(即非常量表达式)的内部sizeof,decltype和其它上下文允许非常量表达式.替换以词汇顺序进行,并在遇到导致演绎失败的条件时停止.



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

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

问题:

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

Fil*_*efp 60

如上所述,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; };
Run Code Online (Sandbox Code Playgroud)

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

template<class> void foo (...);          // fallback
Run Code Online (Sandbox Code Playgroud)

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

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

在标记的行上,(G)我们希望编译器首先检查(E),如果成功进行评估(F),但在本文中讨论的标准更改之前没有这样的保证.


替换的直接背景foo(int)包括;

  • (E)确保传入的T::type
  • (F)确保inner_type<T>::type


如果(F)评估即使(E)导致无效替换,或者(F)(E)我们的简短(愚蠢)示例之前评估不会使用SFINAE,我们将得到诊断说我们的应用程序格式不正确..即使我们打算foo(...)在这种情况下使用.


注意:请注意,这SomeType::type不在模板的直接上下文中; typedef里面的失败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)
Run Code Online (Sandbox Code Playgroud)

auto value = underlying_value (SOME_ENUM_VALUE);                                  // (B)
Run Code Online (Sandbox Code Playgroud)

原始实施

说完了,我们决定编写一个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); }
Run Code Online (Sandbox Code Playgroud)

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

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


代码审查

Don Quixote是一位经验丰富的C++开发人员,一手拿咖啡,另一手拿C++标准.如何用双手忙着编写一行代码是一个谜,但这是一个不同的故事.

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

20.10.7.6 - 其他转换 -[meta.trans.other]

template<class T> struct underlying_type;
Run Code Online (Sandbox Code Playgroud)

条件: T应为枚举类型(7.2)
注释:成员typedef type应命名基础类型T.

注:本标准规定了一个条件underlying_type,但它不会去任何进一步specifiy如果它有一个实例会发生什么不枚举.由于我们不知道在这种情况下会发生什么,因此使用属于未定义的行为 ; 它可能是纯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); }
Run Code Online (Sandbox Code Playgroud)

风车

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

编译为C++ 11我们的实现仍可能导致的实例化std::underlying_type一个T是不列举的原因有两个类型:

  1. 编译器可以自由地评估(D)之前(C)由于置换顺序没有良好定义的,和;

  2. 即使编译器(C)之前评估过(D),也不能保证它不会被评估(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);  
}
Run Code Online (Sandbox Code Playgroud)

注意: underlying_type之所以使用是因为它是一种简单的方法,可以在标准中使用标准中的内容; 重要的是用非枚举实例化它是未定义的行为.

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