为类型特征_t别名和_v变量模板的目的是什么?

What is the purpose of _t aliases and _v variable templates for type traits?

提问人:KamilCuk 提问时间:9/12/2023 最后编辑:Jan SchultkeKamilCuk 更新时间:10/11/2023 访问量:1590

问:

还有很多 和 后缀,如 、 和 milions 等其他此类函数。*_v*_tstd::is_same_vstd::invoke_result_tresult_of_t

它们为什么存在?在任何上下文中公开实现细节(如 或)是否有利?忽略标准合规性,是否应该始终首选版本?这些版本可能根本就不存在吗?std::result_of::typestd::is_same::value_v_t::type::value

C++ 17 C++14 语言设计 类型特征

评论

4赞 Cory Kramer 9/12/2023
它们被添加为嵌套类型中更具可读性的别名。我个人一开始并不觉得嵌套类型特别难读。尽管有一些特定的 linter 可以强制执行这一点。至于你的最后一个问题,不,我认为这并不完全正确,因为归根结底,两者都是依赖类型。::type::valueT

答:

17赞 NathanOliver 9/12/2023 #1

问题在于,当您拥有 时,是一个依赖名称,因此需要对其进行限定,以便通知编译器它是类型而不是对象的名称。这些名称是别名,因此您不必自己键入。std::result_of<TEMPLATE_STUFF>::typetypetypenametype*_ttypename trait_name::typetypename

s 不需要 the,因为这些是值,但它保持一致的语法,并且仍然较少键入。typename*_v

9赞 user17732522 9/12/2023 #2

在任何上下文中公开实现细节(如 std::result_of::type 或 std::is_same::value)是否有用?

它们不是实现细节。这些是要使用的原始接口。

变体必须实现为仅自 C++11 以来存在的别名模板,并且变体必须实现为仅自 C++14 以来存在的变量模板_t_v

因此,最初根本无法定义没有间接 through 或成员的接口。速记版本是后来添加的,除了安全一些键入并使代码更易于阅读之外,什么也没做。::type::value

这些变体可以直接用 C++11 添加,但很早就已经考虑将类型特征包含在 C++11 中。据推测,别名模板是后来添加到草稿中的,或者接口已经很熟悉,因为类型特征实现在 C++11 之前也已经存在。_t::type

如果您正在编写不需要与早期 C++ 版本兼容的 C++ 或更高版本的代码,则(几乎)没有理由不使用 and 变体而不是旧的 / 接口。如果你需要支持较旧的 C++ 版本,那么你应该相应地使用 /,直到分别达到 C++11 和 C++14。_t_v::type::value::type::value

在这种情况下,偶尔使用类型特征本身而不是成员是有用的。由于类型特征被定义为派生自或根据其计算方式派生,因此这允许将逻辑保持在类型级别,而不会走任何弯路。::value::valuestd::true_typestd::false_type

有时,在模板中仅有条件地使用 / 成员也很有用。在这种情况下,它们的优点是,只有当 / 出现在实例化的上下文中时,才需要实例化类型特征。因为实例化可能会导致许多其他实例化,具体取决于类型特征,并且如果类型尚未完成,则可能具有 UB。::value::type::value::type

为了保持一致性,新添加的类型特征仍然遵循相同的方案。

评论

1赞 Ted Lyngmo 9/12/2023
回复:“新添加的类型性状仍然遵循相同的一致性方案”——值得一提的是,某些类型性状依赖于基本(非)性状的存在,例如_vstd::conjunction/std::disjunction
1赞 KamilCuk 9/12/2023
there is no reason to not use the _t and _v variants instead of the old ::type/::value interface总的来说,这就是我正在寻找的答案。但是,我想知道,如果总是首选 C++17,那么它有什么意义呢?可以删除吗?std::invoke_resultstd::invoke_result_tstd::invoke_result
0赞 Enlico 9/12/2023
@KamilCuk,我猜你在另一个答案的底部得到了后续问题的答案。
21赞 Yksisarvinen 9/12/2023 #3

TL;DR - 别名可以显著缩短元编程代码,因为省略了 和 。 后来添加了变量模板,以便与别名对称,因为它们在各个方面都更好。_t::typetypename_v_t

C++17 - 变量模板_v

以下是将变量模板引入 C++17 的提案文件中的一句话:_v

变量模板(如 is_same_v<T、U> 优于嵌套模板 像 is_same<T、U>::value 这样的常量有几个原因:

  1. 显然,前者短了 5 个字符。

  2. 不太明显的是,前者消除了“不连贯的冗长”。在处理类型特征时,通常不得不说这样的话 enable_if_t<!is_same<X, decay_t>::value, Z>, 其中 “is_same” 和 “::value”之间可能有大量的 机械。

  3. 不得不说“::value”不是一个功能,它是已经解决的语言限制的解决方法。(请注意,此提案 不触及结构模板,所以想要 is_same类型(而不是其嵌套常量)不受影响。

此外,采用文件中的要点:

随着类型特征别名模板的成功,已经提出了一组匹配的变量模板。对变量模板的语言支持来得太晚了,无法自信地对 C++14 进行此更改,但此后的经验表明,这种变量模板更加简洁,可以以类似于别名通过标准被广泛采用的方式清理文本,并且作者在他自己的标准类型特征库实现中使用它们的经验是,使用此类变量编写代码时要简单得多模板,而不是将值转换为类型,然后对类型执行模板操作,然后再将类型转换回值。
对标准的影响是,许多引用的地方会改用 .节省的费用不如别名模板那么大,因为没有令人讨厌的删除。然而,使用和指代特征,而不是用来提取意义的一致性是令人信服的。
_t_v_tsome_trait<T>::valuesome_trait_v<T>typename_t_v::something

C++14 - 别名_t

在C++14中引入别名的论文中提供了类似的推理,并增加了NathanOliver在他的回答中评论的额外好处。引用论文:_ttypename

不幸的是,对于最常见的用例来说,上述灵活性是有代价的。 在模板上下文中,C++ 要求对元函数的每个“元调用”都具有语法开销 以介绍性关键字的形式,以及后缀:typename::type

typename metafunction-name<metafunction-argument(s)>::type

即使是相对简单的构图也会很快变得有些混乱;更深 嵌套是彻头彻尾的笨拙:

template< class T > using reference_t
   = typename conditional<is_reference<T>::value, T,
                          typename add_lvalue_reference<T>::type>::type;

更糟糕的是,意外省略关键字可能会导致对程序员来说晦涩难懂的诊断 谁不擅长元编程细节。

根据我们的经验,传递元函数(而不是元数据)构成了相对 一小部分元功能组成。我们发现自己传递了元函数结果 更频繁。因此,我们建议为库添加一组模板别名,以减轻程序员表达更多内容的负担 常见情况。请注意,在上述示例的以下重写中,没有任何关键字,也没有任何后缀,因此将语句从 3 压缩到 2 代码行数:TransformationTraitstypename::type

template< class T > using reference_t
    = conditional_t< is_reference<T>::value, T, add_lvalue_reference_t<T> >;
7赞 Jan Schultke 9/12/2023 #4

别名模板在 C++14 中引入,变量模板在 C++17 中引入。这些存在的原因有很多。_t_v

和模板更方便。_t_v

首先,五个字符是否比 .此外,您需要在后者前面加上前缀,因为编译器无法推断是类型成员还是静态成员。另请参阅我必须在哪里以及为什么必须放置“模板”和“typename”关键字?trait_t<T>trait<T>::typetypename::type

比较 C++11/C++17 代码,这可能会有很大的不同:

// C++17
template <typename T>
std::enable_if_t<!std::is_void_v<T>> foo();
// C++11
template <typename T>
typename std::enable_if<!std::is_void<T>::value>::type foo();

和模板提供了更大的实现自由度。_t_v

特征都是类这一事实是一个重大限制。这意味着每次使用例如 将不得不实例化一个新的类模板,而这相对昂贵。现代编译器将所有类型特征实现为内部函数,类似于:std::is_same

template <typename _A, typename _B>
struct is_same {
    static constexpr bool value = __is_same(_A, _B);
};

template <typename _A, typename _B>
inline constexpr bool is_same_v = __is_same(_A, _B);

请参阅 libc++ 中的 __type_traits/is_same.h

该类显然是多余的,直接通过变量模板使用内置函数会更有效。

特征类现在已经没有意义了吗?_t_v

答案取决于编译器。 支持这些类的一个常见论点是它们允许短路。 例如,您可以替换

(std::is_same_v<Ts, int> && ...)
// with
std::conjunction_v<std::is_same<Ts, int>...>

...与折叠表达式不同,并非所有表达式都会被实例化。 然而,至少对于 clang 和 GCC 来说,实例化的成本非常便宜(由于它是内置的),即使折叠表达式不会短路,使用它们仍然会提高编译速度。std::is_samestd::is_same_v

benchmark results showing that the use of fold expressions is 1.2 times faster than std::conjunction/std::disjunction
点击图片进入基准测试结果

但是,较旧的编译器可能会使用实际类而不是内置类来实现某些特征,因此理论上短路可能会更好。

特征类有时对 TMP 更方便。

无论性能如何,这些特征有时对元编程都很有用,例如:

template <typename T>
struct Select;

template <typename A, typename B>
struct Select<Pair<A, B>> : std::conditional<LeftIsBetter<A, B>, A, B> {};
// is more concise than
template <typename A, typename B>
struct Select<Pair<A, B>> {
    using type = std::conditional_t<LeftIsBetter<A, B>, A, B>;
};

在某些情况下,从 trait 类继承很方便,尽管不是绝对必要的。

另见

您可以在以下论文中找到别名/变量模板的基本原理和进一步解释:_t_v

评论

2赞 rubenvb 9/12/2023
请注意,命名约定实际上是由 POSIX 编码的,它保留了所有以 for typenames 结尾的名称(当然,在 C++ 和命名空间中不应该干扰)。_t_t
0赞 KamilCuk 9/13/2023
are sometimes useful for metaprogramming, such as:澄清一下,不可能写 using ?或者生成的代码是不可读的?我能够写这个 godbolt.org/z/oMn5EsMbY,它看起来还不错,但我不知道如何正确匹配。我的观点是,如果你可以用形式来表达一切,或者反过来,为什么同时拥有两个接口?简明扼要是一个论点。Select<Pair<A, B>template<...> using Select_t = ...template<std::pair T>_t::type
1赞 Jan Schultke 9/13/2023
@KamilCuk这之所以有效,是因为您依赖于具有 A 和别名的类型。通常无法通过别名方便地获取有关类型的属性。例如,如果要推断数组的大小或类型等属性,则没有可用的别名,并且必须使用类。在特定情况下,有 和 ,但情况并非总是如此。该示例只是为了演示在此类事件中该怎么做。::first_type::second_typestd::pair::first_type::second_type