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

C ++ 11

14.8.2 – 模板参数推导[temp.deduct]

7replace发生在函数types和模板参数声明中使用的所有types和expression式中。 expression式不仅包含常量expression式,例如出现在数组边界中的常量expression式或非types模板参数,还包括sizeofdecltype和其他允许非常量expression式的上下文中的常规expression式(即非常量expression式)。


C ++ 14

14.8.2 – 模板参数推导[temp.deduct]

7replace发生在函数types和模板参数声明中使用的所有types和expression式中。 expression式不仅包含常量expression式,例如出现在数组边界中的常量expression式或非types模板参数,还包括sizeofdecltype和其他允许非常量expression式的上下文中的常规expression式(即非常量expression式)。 replace按照词汇顺序进行,并在遇到导致扣除失败的条件时停止



添加的句子在处理C ++ 14中的模板参数时明确指出了replace的顺序。

替代的顺序是最经常不被重视的。 我还没有find一个关于为什么这个问题的文件。 也许这是因为C ++ 1y还没有完全标准化,但我认为这样的改变一定是出于某种原因而被引入的。

问题是:

  • 为什么和什么时候,模板论证replace的顺序很重要?

如前所述,C ++ 14明确指出,模板参数replace的顺序是明确的; 更具体地说,将保证按照词汇顺序进行,并在替代导致扣除失败时停止。

与C ++ 11相比,编写SFINAE代码要容易得多,这个代码由C ++ 14中的另一个规则组成,我们也将远离模板replace的未定义sorting可能使我们的整个应用程序受到影响的情况未定义行为。

注意 :重要的是要注意,C ++ 14中描述的行为一直是预期的行为,即使在C ++ 11中也是这样,因为它没有以这种明确的方式expression出来。



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

造成这种变化的最初原因可以在DanielKrügler最初提交的缺陷报告中 find

  • C ++标准核心语言缺陷报告和被接受的问题,修订88
    • 1227.在扣除失败中混合直接和非直接语境

进一步的解释

在编写SFINAE时,我们作为开发人员依赖编译器来查找在使用时会在我们的模板中产生无效typesexpression式的replace。 如果发现这样的无效实体,我们想忽略模板声明的任何内容,并希望find适合的匹配。

换人失败不是一个错误 ,而是一个单纯的.. “哇,这是行不通的,请继续前进”

问题是潜在的无效types和expression式只能在替代的直接上下文中find。

14.8.2 – 模板参数推导[temp.deduct]

8如果replace导致无效的types或expression式,则键入演绎失败。 无效的types或expression式是使用replace参数编写的格式不正确的types或expression式。

[ 注意:访问检查是替代过程的一部分。 – 注意 ]

只有函数types及其模板参数types的上下文中的无效types和expression式才会导致推理失败。

[ 注意:replacetypes和expression式的评估可能导致副作用,例如类模板特化和/或函数模板特化的实例化,隐式定义函数的生成等。这些副作用不在“立即上下文“,并可能导致程序不合格。 – 注意 ]

换句话说,在非直接语境中发生的replace仍然会使程序不合格,这就是为什么模板replace的顺序很重要; 它可以改变某个模板的全部含义。

更具体地说,它可以是具有在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)中的replace的直接上下文包括:

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

如果(F)被评估,即使(E)导致无效替代,或者如果(F)(E)之前被评估,我们的短(愚蠢)例子将不使用SFINAE,并且我们将得到诊断,应用程序是不健全的,即使我们打算在这种情况下使用foo(...)

注意:注意SomeType::type不在模板的直接上下文中; inner_type内的typedef inner_type将导致应用程序不合格,并阻止模板使用SFINAE



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

这种改变将极大地减轻语言律师试图实施某些保证以某种方式(和秩序)进行评估的事情的生活,而不pipe他们使用的编译器是什么。

它也将使模板论证替代以非语言律师更自然的方式performance出来; 从左到右的replace发生得比直接编译器想要做的更加直观– … –


没有任何负面的暗示?

我唯一能想到的是,由于replace顺序将从左到右发生,编译器不允许使用asynchronous实现一次处理多个replace。

我还没有碰到这样的实现,我怀疑这会导致任何重大的性能收益,但至less(在理论上)思想适合于事物的“负面”方面。

举一个例子:编译器将无法使用两个同时进行replace的线程,而不需要任何机制来实现特定模板的动作,就像在某个特定点从未发生时所做的replace一样(如果需要的话)。



故事

注意 :本节将介绍一个本来可以从现实生活中获得的例子,以描述模板参数replace顺序的重要性。 请让我知道(使用评论部分),如果有什么不够清楚,甚至可能是错误的。

想象一下,我们正在使用枚举器 ,我们想要一种方法来轻松获得指定枚举基础

基本上我们厌倦了总是写(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 ++标准。 他双手忙碌地写了一行代码是一个谜,但这是一个不同的故事。

他回顾了我们的代码,得出结论认为实现是不安全的,因为我们可以传递一个不是枚举typesT ,所以我们需要防止std::underlying_type来自undefined-behavior。

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

 template<class T> struct underlying_type; 

条件: T应为枚举types(7.2)
注释:成员typedef type应该命名T的基本types。

注意:标准为underlying_type指定了一个条件 ,但是它不会进一步规定如果使用非枚举实例化会发生什么。 由于我们不知道在这种情况下会发生什么,使用属于未定义的行为 ; 它可能是纯粹的UB ,使应用程序不合格,或在网上订购可食用的内衣。


骑士的骑士

Don大声疾呼我们应该如何遵守C ++标准,而且我们应该为我们所做的事情感到非常遗憾..这是不可接受的。

在他平静下来,又喝了几口咖啡之后,他build议我们改变实施方式,以防止用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); } 

WINDMILL

我们感谢Don的发现,现在对我们的实现感到满意,但直到我们意识到模板参数replace的顺序在C ++ 11中没有明确定义(也就是说在replace停止的时候也没有说明)。

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

  1. (C) (D)之前,编译器可以自由地评估(D) ,因为replace顺序没有明确定义,

  2. 即使编译器在(D) (C)之前评估(C) ,但不能保证它不会评估(D) ,C ++ 11没有明确说明replace链何时停止的子句。


Don的实现将在C ++ 14中免于未定义的行为 ,但仅仅是因为C ++ 14明确声明replace将按照词法顺序进行 ,并且只要replace导致推导失败就会停止

唐可能不会在这台风机上打架,但他肯定错过了C ++ 11标准中的一条非常重要的龙。

在C ++ 11中一个有效的实现需要确保不pipe模板参数的replace发生的顺序如何, std::underlying_typestd::underlying_type不会是一个无效的types。

 #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 ,因为这是一种简单的方式来使用标准中的某些内容来对抗标准中的内容。 非常重要的一点是用非枚举实例化它是未定义的行为

之前在这篇文章中提到缺陷报告使用了一个更为复杂的例子,它假设了关于这个问题的广泛的知识。 我希望这个故事对那些对这个主题不熟悉的人来说是一个更合适的解释。