My goal is to implement a predicate that detects the presence of a nested using
alias (or typedef
) that acts as a light-weight tag to indicate that a class has some attribute (for the purposes of generic programming). For example, a has_my_tag<T>
predicate should behave as follows:
struct A {
using my_tag = void;
};
struct B {};
int main()
{
static_assert(has_my_tag<A>::value, ""); // evaluate to true if my_tag=void is present
static_assert(!has_my_tag<B>::value, ""); // false otherwise
}
User @JoelFalcou called this the "lightweight type categorization idiom" and provided a solution in this answer. I have been unable to find any references for an idiom of that name (do you know of any?) Here's Joel's implementation of has_my_tag<>
:
template<class T, class R = void>
struct enable_if_type { typedef R type; };
template<class T, class Enable = void>
struct has_my_tag : std::false_type {};
template<class T>
struct has_my_tag<T, typename enable_if_type<typename T::my_tag>::type> :
std::true_type
{};
And here is a working version on the Compiler Explorer: https://godbolt.org/z/EEOBb-
I have come up with the following simplified version:
template<class T, class Enable = void>
struct has_my_tag : std::false_type {};
template<class T>
struct has_my_tag<T, typename T::my_tag> : std::true_type
{};
https://godbolt.org/z/yhkHp7
My Questions: Is the simplified version an acceptable way to implement the idiom? Are there circumstances where it would fail? Is there a simpler version that works in C++11? Which version should I prefer?
From what I understand, Joel's version would allow my_tag
to alias any type, whereas my version requires my_tag
to alias void
. But given the goal of tagging types for light-weight predicate testing, I am not clear which version is to be preferred.
Auxiliary questions: Also, are there other names for this idiom? Is it used in any libraries that I could investigate? So far I have not found a name that brings up any search results.
For your setup, there is no difference between the original version and yours. Both use SFINAE to select the correct has_my_tag. Your version does however constrain your typedef/using
to be my_tag=void
. if my_tag is typedef'd as any other type, your specialization will not match, and you will end up instantiating the primary template as can be seen here.
The reason for this is that where you instanciate the templates in main, static_assert(has_my_tag<A>::value, "");
you are not specifying the second parameter, so the default (void) is used, that is has_my_tag<A,void>::value
Your specialization must match this to be considered.
The usage of enable_if_type
, (basically doing the job of void_t in c++17) is to enable SFINAE on the ::type
member of T, but then always result in void, such that your specialization will match when ::type
exists, regardless of the type of the my_tag
typedef.
This allows you to just worry about whether it exists, and not its type;
Personally I would use the approach that doesn't depend on my_type
being typedef'd as void, either the enable_if_type version, or something like...
#include <iostream>
#include <type_traits>
struct A {
using my_tag = void;
};
struct B {};
struct C {
using my_tag = int; // with void_t also works with my_tag = int
};
struct D {
struct my_tag{}; //or some struct as the tag
};
// same as your enable_if_type
template <typename...>
using void_t = void;
template<class T, class Enable = void>
struct has_my_tag : std::false_type {};
template<class T>
struct has_my_tag<T, void_t<typename T::my_tag>> : std::true_type
{};
int main() {
std::cout << has_my_tag<A>::value << std::endl;
std::cout << has_my_tag<B>::value << std::endl;
std::cout << has_my_tag<C>::value << std::endl;
std::cout << has_my_tag<D>::value << std::endl;
return 0;
}
Demo
First, yours works, but does depend on it being void
. That lightweight tag might be useful if it carried a non-void type in some situations, and if it is non-void you silently get detection failing, which seems bad.
Second, your type tagging require you to modify the type, which means you cannot get built-in or types you don't own (like ones in std
). We can fix this.
namespace type_tag {
namespace adl {
template<template<class...>class tag>
struct tag_token_t {};
template<class T, template<class...> class tag>
constexpr
decltype( (void)(std::declval<tag<T>>()), std::true_type{} )
tag_test( T*, tag_token_t<tag> ) { return {}; }
template<class T, template<class...> class tag, class...LowPriority>
constexpr
std::enable_if_t<!std::is_same<T,int>{}, std::false_type> tag_test(T*, tag_token_t<tag>, LowPriority&&...) {
return {};
}
}
template<template<class...>class Z>using tag_token_t = adl::tag_token_t<Z>;
template<template<class...>class tag>
constexpr tag_token_t<tag> tag_token{};
namespace details {
template<class T, template<class...>class tag>
constexpr auto test_impl( T*, tag_token_t<tag> ) {
return tag_test( (T*)nullptr, tag_token<tag> );
}
}
template<class T, template<class>class tag>
constexpr auto tag_test() {
return details::test_impl((T*)nullptr, tag_token<tag>);
}
}
so now a tag is this:
template<class T>
using my_tag = typename T::my_tag;
we can test it as follows:
constexpr auto double_has_tag = type_tag::tag_test< double, my_tag >();
which returns a compile-time true or false if double
has a tag.
We can decide that int
has a tag by doing:
namespace type_tag::adl {
constexpr std::true_type tag_test( int*, type_tag::tag_token_t<my_tag> ) {
return {};
}
}
or, for types that we control:
struct my_tagged_type {
using my_tag = void;
};
for types we can inject names into their namespace (ie, not std
or built-in types) we can do:
namespace not_my_ns {
constexpr std::true_type tag_test( not_my_type*, ::tag_test::tag_token_t<::my_tag> ) {
return {};
}
}
and suddenly type_tag::tag_test<not_my_ns::not_my_type, ::my_tag>()
is truthy.
Once we have tag_test< type, tag_name >()
we can use the usual std::enable_if
instead of some custom system.
The advantages of this system include:
It can be extended without changing anything about the type you are tagging.
It can be extended using the using tag=void;
or using tag=int;
that your system works with.
At SFINAE point of use, it is just another compile-time bool
. So your existing SFINAE patterns work with it.
If you pick a poor name for the tag type in the struct that someone else is using for an unrelated reason, tag_test
can override this on a per-type basis.
The disadvantage is that it took a bit of magic to do this. But in common use cases, you get the same work required for end-users as your lightweight system. In more complex use cases, this lets you do things your lightweight one won't.
Live example.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With