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

4

16

There are a lot of *_v and *_t suffixes, like std::is_same_v, std::invoke_result_t, result_of_t and milions of other such functions.

Why do they exist at all? Is it beneficial in any context to expose implementation details like std::result_of::type or std::is_same::value? Ignoring standard compliance, should the _v _t versions always be preferred? Could the ::type ::value versions have never existed at all?

Rood answered 11/9, 2023 at 17:32 Comment(1)
They were added as more-readable aliases to the nested types. I personally don't find the nested types particularly hard to read in the first place. Though there are particular linters that will enforce this. As far as your last question, no I don't think that's quite true as at the end of the day both ::type and ::value are dependent types on T.Quittor
D
7

The _t alias templates were introduced in C++14 and the _v variable templates in C++17. There are many good reasons for why these exist.

The _t and _v templates are more convenient.

Firstly, trait_t<T> is five characters shorter than trait<T>::type. Furthermore, you would need to prefix the latter with typename because the compiler cannot infer whether ::type is a a type or a static member. See also Where and why do I have to put the "template" and "typename" keywords?.

This can make a big difference, comparing C++11/C++17 code:

// 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();

The _t and _v templates offer more implementation freedom.

The fact that the traits are all classes is a significant limitation. It means that each use of e.g. std::is_same will have to instantiate a new class template, and this is relatively costly. Modern compilers implement all type traits as intrinsics, similar to:

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);

See __type_traits/is_same.h in libc++.

The class is obviously redundant and it would be much more efficient to use the built-in function directly through a variable template.

Are trait classes pointless now that _t and _v exist?

The answer depends on the compiler. A common argument in favor of the classes is that they allow short-circuiting. For example, you can replace

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

... and unlike the fold expression, not all std::is_same will be instantiated. However, at least for clang and GCC, the cost of instantiating std::is_same_v is so trivially cheap (thanks to it being a built-in) that even though fold expressions don't short-circuit, it's still better for compilation speed to use them.

benchmark results showing that the use of fold expressions is 1.2 times faster than std::conjunction/std::disjunction
Click on image to go to benchmark results

However, older compilers might implement some traits using actual classes instead of built-ins, so it's theoretically possible that short-circuiting would be better.

Trait classes are sometimes more convenient for TMP.

Regardless of performance, the traits are sometimes useful for metaprogramming, such as:

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

Inheriting from trait classes is convenient in some cases, although not strictly necessary.

See Also

You can find rationale and further explanation of _t and _v alias/variable templates in the following papers:

Dibbell answered 12/9, 2023 at 10:3 Comment(3)
note that the _t naming convention is actually encoded by POSIX which reserves all names ending in _t for typenames (which in C++ and with namespaces shouldn't interfere of course).Border
are sometimes useful for metaprogramming, such as: To clarify, it is not possible to write the Select<Pair<A, B> using template<...> using Select_t = ...? Or the resulting code is then not readable? I was able to write this godbolt.org/z/oMn5EsMbY , and it looks quite nice, but I do not know how to match template<std::pair T> properly. My point is, if you can express everything with _t form as with ::type, or the other way round, why have both interfaces? Concise is an argument.Rood
@Rood this only works because you're relying on the type having a ::first_type and ::second_type alias. The properties you obtain about a type are often not conveniently obtained through aliases. For example, if you're deducing properties such as the size or type of an array, then there are no aliases available, and you must use a class. In the specific case of std::pair, there is ::first_type and ::second_type, but that's not always the case. The example is just meant to demonstrate what to do in such an event.Dibbell
S
21

TL;DR - _t aliases can shorten metaprogramming code significantly due to omitting ::type and typename. _v variable templates were added later for symmetry with _t aliases and because they're just better in every way.

C++17 - _v variable templates

Here's a quote from proposal paper that introduced _v variable templates into C++17:

Variable templates like is_same_v<T, U> are superior to nested constants like is_same<T, U>::value for several reasons:

  1. Obviously, the former is 5 characters shorter.

  2. Less obviously, the former eliminates "disconnected verbosity". When working with type traits, it's common to have to say things like enable_if_t<!is_same<X, decay_t>::value, Z>, where "is_same" and "::value" are separated by a potentially substantial amount of machinery.

  3. Having to say "::value" isn't a feature, it's a workaround for a language limitation that has been solved. (Note that this proposal doesn't touch the struct templates, so metaprogrammers who want the is_same type instead of its nested constant are unaffected.)

Also, important points from the adoption paper:

Following the success of the _t alias templates for type traits, a matching set of _v variable templates have been proposed. The language support for variable templates arrived too late to confidently make this change for C++14, but experience since has shown that such variable templates are more succinct, can clean up the text in a similar way that the _t aliases have been widely adopted through the standard, and the author's experience using them in his own implementation of the standard type traits library is that code is much simpler when written using such variable templates directly, rather than turning a value into a type, then performing template manipulations on the type, before turning the type back into a value.
The impact on the standard is that many places that reference some_trait<T>::value would instead use some_trait_v<T>. The saving is not quite as great as in the case of alias templates, as there is no irksome typename to remove. However, the consistecy of using _t and _v to refer to traits, and not using ::something to extract meaning is compelling.

C++14 - _t aliases

Similar reasoning was provided in paper that introduced _t aliases in C++14, with the extra benefit of adding typename, which NathanOliver remarked in his answer. Quote from the paper:

Unfortunately, the above-described flexibility comes with a cost for the most common use cases. In a template context, C++ requires that each “metacall” to a metafunction bear syntactic overhead in the form of an introductory typename keyword, as well as the suffixed ::type:

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

Even relatively straightforward compositions can rather quickly become somewhat messy; deeper nesting is downright unwieldy:

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

Worse, accidentally omitting the keyword can lead to diagnostics that are arcane to programmers who are inexpert in metaprogramming details.

In our experience, passing metafunctions (rather than metadata) constitutes a relatively small fraction of metafunction compositions. We find ourselves passing metafunction results far more frequently. We therefore propose to add a set of template aliases for the library’s TransformationTraits in order to reduce the programmer burden of expressing this far more common case. Note, in the following rewrite of the above example, the absence of any typename keyword, as well as the absence of any ::type suffix, thus condensing the statement from 3 to 2 lines of code:

template< class T > using reference_t
    = conditional_t< is_reference<T>::value, T, add_lvalue_reference_t<T> >;
Sclerosis answered 11/9, 2023 at 17:55 Comment(0)
G
17

The issue is that when you have std::result_of<TEMPLATE_STUFF>::type, type is a dependent name and as such needs to be qualifed with typename in order to inform the compiler that type is the name of a type and not a object. The *_t names are aliases to typename trait_name::type so you don't have to type typename yourself.

The typename is not needed for the *_vs since those are values, but it keeps a consistent syntax and is still less typing.

Gulosity answered 11/9, 2023 at 17:41 Comment(0)
C
9

Is it beneficial in any context to expose implementation details like std::result_of::type or std::is_same::value?

They are not implementation details. These are the original interfaces that are intended to be used.

The _t variants have to be implemented as alias templates which only exist since C++11 and the _v variants have to be implemented as variable templates which only exist since C++14.

So originally it simply wasn't possible to define the interface without the indirection through ::type or ::value members. The short-hand versions have been added later and don't do anything but safe some typing and making code easier to read.

The _t variants could have been added directly with C++11, but the type traits had already been considered for inclusion into C++11 very early on. Presumably alias templates were added to the draft later or the ::type interface was already familiar because type trait implementations have existed before C++11 as well.

If you are writing C++17 or later code that doesn't need to be compatible with earlier C++ versions, there is (almost) no reason to not use the _t and _v variants instead of the old ::type/::value interfaces. If you need to support older C++ versions, then you should use ::type/::value accordingly, until up to C++11 and C++14 respectively.

In case of ::value it is occasionally useful to use the type trait itself rather than the ::value member. Because the type trait is defined to be derived from std::true_type or std::false_type according to how it evaluates, this allows one to keep the logic at the type level without any detours.

Sometimes it is also useful to use the ::value/::type members only conditionally in templates. In that case they have the advantage that the type trait needs to be instantiated only when ::value/::type appears in an instantiated context. Because the instantiation can, depending on the type trait, cause many other instantiations and might have UB if the type is not yet complete.

Newly-added type traits still follow the same scheme for consistency.

Cryptozoite answered 11/9, 2023 at 17:45 Comment(3)
Re: "Newly-added type traits still follow the same scheme for consistency" - it may be good to mention that some type traits depend on the base (non-_v) trait to exist, like std::conjunction/std::disjunctionSprawl
there is no reason to not use the _t and _v variants instead of the old ::type/::value interface Overall, this is the answer I am looking for. But then,, I wonder, what is the point of std::invoke_result available from C++17 if std::invoke_result_t should always be preferred? Could std::invoke_result be removed?Rood
@KamilCuk, I guess you got the answer to the follow up question towards the bottom of the other answer.Ding
D
7

The _t alias templates were introduced in C++14 and the _v variable templates in C++17. There are many good reasons for why these exist.

The _t and _v templates are more convenient.

Firstly, trait_t<T> is five characters shorter than trait<T>::type. Furthermore, you would need to prefix the latter with typename because the compiler cannot infer whether ::type is a a type or a static member. See also Where and why do I have to put the "template" and "typename" keywords?.

This can make a big difference, comparing C++11/C++17 code:

// 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();

The _t and _v templates offer more implementation freedom.

The fact that the traits are all classes is a significant limitation. It means that each use of e.g. std::is_same will have to instantiate a new class template, and this is relatively costly. Modern compilers implement all type traits as intrinsics, similar to:

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);

See __type_traits/is_same.h in libc++.

The class is obviously redundant and it would be much more efficient to use the built-in function directly through a variable template.

Are trait classes pointless now that _t and _v exist?

The answer depends on the compiler. A common argument in favor of the classes is that they allow short-circuiting. For example, you can replace

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

... and unlike the fold expression, not all std::is_same will be instantiated. However, at least for clang and GCC, the cost of instantiating std::is_same_v is so trivially cheap (thanks to it being a built-in) that even though fold expressions don't short-circuit, it's still better for compilation speed to use them.

benchmark results showing that the use of fold expressions is 1.2 times faster than std::conjunction/std::disjunction
Click on image to go to benchmark results

However, older compilers might implement some traits using actual classes instead of built-ins, so it's theoretically possible that short-circuiting would be better.

Trait classes are sometimes more convenient for TMP.

Regardless of performance, the traits are sometimes useful for metaprogramming, such as:

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

Inheriting from trait classes is convenient in some cases, although not strictly necessary.

See Also

You can find rationale and further explanation of _t and _v alias/variable templates in the following papers:

Dibbell answered 12/9, 2023 at 10:3 Comment(3)
note that the _t naming convention is actually encoded by POSIX which reserves all names ending in _t for typenames (which in C++ and with namespaces shouldn't interfere of course).Border
are sometimes useful for metaprogramming, such as: To clarify, it is not possible to write the Select<Pair<A, B> using template<...> using Select_t = ...? Or the resulting code is then not readable? I was able to write this godbolt.org/z/oMn5EsMbY , and it looks quite nice, but I do not know how to match template<std::pair T> properly. My point is, if you can express everything with _t form as with ::type, or the other way round, why have both interfaces? Concise is an argument.Rood
@Rood this only works because you're relying on the type having a ::first_type and ::second_type alias. The properties you obtain about a type are often not conveniently obtained through aliases. For example, if you're deducing properties such as the size or type of an array, then there are no aliases available, and you must use a class. In the specific case of std::pair, there is ::first_type and ::second_type, but that's not always the case. The example is just meant to demonstrate what to do in such an event.Dibbell

© 2022 - 2024 — McMap. All rights reserved.