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

C ++ 11

14.8.2-模板参数推导-sizeof

7替换发生在函数类型和模板参数声明中使用的所有类型和表达式中。 这些表达式不仅包括常量表达式(例如出现在数组边界中或作为非类型模板参数的常量表达式),还包括sizeofdecltype内部的通用表达式(即非常量表达式),以及允许非常量表达式的其他上下文。


C ++ 14

14.8.2-模板参数推导-sizeof

7替换发生在函数类型和模板参数声明中使用的所有类型和表达式中。 这些表达式不仅包括常量表达式(例如出现在数组边界中或作为非类型模板参数的常量表达式),还包括sizeofdecltype内部的通用表达式(即非常量表达式),以及允许非常量表达式的其他上下文。 替换以词法顺序进行,并在遇到导致推论失败的条件时停止。



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

替换顺序通常很少引起注意。 我还没有找到关于这为何重要的论文。 也许是因为C ++ 1y尚未完全标准化,但是我认为必须引入这种更改是有原因的。

问题:

  • 为什么以及何时,模板参数替换的顺序很重要?
1个解决方案
59 votes

如前所述,C ++ 14明确指出模板参数替换的顺序是明确定义的; 更具体地说,将保证以“词法顺序”进行并在替换导致推论失败时停止。

与C ++ 11相比,在C ++ 14中编写由一个规则依赖于另一个规则的SFINAE代码要容易得多,我们还将避免模板替换的不确定顺序可能使我们的整个应用程序遭受痛苦的情况 未定义的行为。

注意:必须注意,C ++ 14中描述的行为一直是预期的行为,即使在C ++ 11中也是如此,只是它的措辞没有这么明确。



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

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

  • C ++标准核心语言缺陷报告和可接受的问题,修订版88
    • 1227.在推论失败中混合即时和非即时上下文

进一步说明

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

替换失败不是一个错误,而仅仅是一个..“哦,这没有用..请继续。”

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

14.8.2-模板参数推导-underlying_type

8如果替换导致无效的类型或表达式,则类型推导将失败。 无效的类型或表达式是使用替换参数编写的格式或表达式。

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

只有在函数类型及其模板参数类型的直接上下文中的无效类型和表达式才能导致推论失败。

[注意:对替换类型和表达式的求值可能会导致副作用,例如实例化类模板专业化和/或函数模板专业化,生成隐式定义的函数等。此类副作用不在“立即 上下文”,并可能导致程序格式错误。 -尾注]

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

更具体地说,可能是拥有在SFINAE中可用的模板与没有在SFINAE中可用的模板之间的区别。


愚蠢的例子

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"
}

在标记为underlying_type的行上,我们希望编译器首先检查type,如果成功,则评估为T,但是在本文讨论的标准更改之前,没有这样的保证。


underlying_type中替换的直接上下文包括;

  • underlying_type确保传入的type具有T
  • underlying_type确保type具有T


如果即使type导致无效替换也评估了underlying_type,或者如果在(E)之前评估了T,则我们简短的示例(愚蠢的示例)将不会使用SFINAE,并且我们将得到诊断信息,表明我们的应用程序格式不正确。 尽管我们打算在这种情况下使用foo(...)


注意:请注意underlying_type不在模板的直接上下文中。 type内部的typedef错误会导致应用程序格式错误,并阻止模板使用SFINAE。



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

这项更改将极大地简化语言律师的生活,无论他们使用的是哪种编译器,这些律师都希望以某种方式(和顺序)实施可以保证以某种方式(和顺序)进行评估的内容。

对于非语言律师来说,这还将使模板参数替换的行为更加自然。 从左到右进行替换远比像erhm那样的编译器想要像erhm那样直观得多。


没有负面影响吗?

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

我还没有偶然发现这种实现方式,并且我怀疑这样做是否会带来任何重大的性能提升,但是至少(从理论上)这种想法有点适合事物的“消极”方面。

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



故事

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

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

基本上,当我们理想地想要更接近type的东西时,我们总是不得不写underlying_type感到厌烦。

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

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

原始实现

说完了,我们决定编写一个underlying_type的实现,如下所示。

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

这将减轻我们的痛苦,并且似乎可以完全满足我们的要求。 我们传入一个枚举数,然后取回基础值。

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


代码审查

唐吉x德(Don Quixote)是一位经验丰富的C ++开发人员,一只手捧着一杯咖啡,另一只手捧着C ++标准。 他如何双手忙于编写一行代码,这是一个谜,但这是另一回事。

他查看了我们的代码并得出结论,该实现是不安全的,我们需要防止underlying_type发生不确定的行为,因为我们可以传入不是枚举类型的type

20.10.7.6-其他转换-underlying_type

underlying_type

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

注意:该标准为underlying_type指定了一个条件,但它并没有进一步说明如果使用非枚举实例化它将发生什么。 由于我们不知道在这种情况下会发生什么,因此使用情况属于不确定行为; 它可以是纯UB,可以使应用格式不正确,也可以在线订购可食用的内衣。


护甲骑士

Don对我们始终应遵守C ++标准大喊大叫,对于所做的事情,我们应该感到极大的耻辱。这是不可接受的。

在他冷静下来并喝了几口咖啡之后,他建议我们更改实现以添加保护,以禁止使用不允许的实例化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时,我们的实现仍会导致underlying_type实例化为(D),该实例不是枚举类型,原因有两个:

  1. 编译器可以在(D)之前自由评估underlying_type,因为替换顺序定义不明确;并且;

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


Don的实现将在C ++ 14中摆脱不确定行为的束缚,但这仅是因为C ++ 14明确声明替换将以词法顺序进行,并且每当替换导致推论失败时它将停止。

Don可能没有为此而战,但是他肯定错过了C ++ 11标准中一条非常重要的龙。

C ++ 11中的有效实现将需要确保无论模板参数替换发生的顺序如何,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,是因为它是将标准中的内容与标准中的内容相比较的一种简单方法。 重要的一点是,使用非枚举实例化它是未定义的行为。

先前在这篇文章中链接的缺陷报告使用了一个更为复杂的示例,该示例假定您对此事有广泛的了解。 我希望这个故事对于那些不太了解该主题的人来说是一个更合适的解释。

Filip Roséen - refp answered 2019-11-15T01:54:04Z
translate from https://stackoverflow.com:/questions/22368022/why-does-the-order-of-template-argument-substitution-matter