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?
1
4 Answers
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 *_v
s since those are values, but it keeps a consistent syntax and is still less typing.
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:
Obviously, the former is 5 characters shorter.
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.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 referencesome_trait<T>::value
would instead usesome_trait_v<T>
. The saving is not quite as great as in the case of alias templates, as there is no irksometypename
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 introductorytypename
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 anytypename
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> >;
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 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.
Newly-added type traits still follow the same scheme for consistency.
3
-
1
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, likestd::conjunction
/std::disjunction
– Ted Lyngmo23 hours ago
-
1
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 ofstd::invoke_result
available from C++17 ifstd::invoke_result_t
should always be preferred? Couldstd::invoke_result
be removed?– KamilCuk8 hours ago
-
@KamilCuk, I guess you got the answer to the follow up question towards the bottom of the other answer.
– Enlico7 hours ago
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 were necessary.
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.
Are the 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.
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.
The 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 the classes is convenient in a some cases, although not strictly necessary.
See Also
You can find rationale and further explanation of _t
and _v
aliase/variable templates in the following papers:
1
-
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).– rubenvb7 hours ago
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 onT
.yesterday