6

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.

Ross Bencina
  • 3,822
  • 1
  • 19
  • 33
  • Whether "the simplified version an acceptable way to implement the idiom" obviously depends entirely on whether it works correctly. That's your decision to make, alone. Does it work correctly, for your intended purposes, or not. This is something you can simply write test cases for, and if the end result is correct, then it is "an acceptable way to implement this idiom". You should not expect anyone to tell you "which version should [you] prefer". You can make that decision yourself. And, lastly, as far as whether there are "other names" for this: unfortunately I don't play buzzword bingo. – Sam Varshavchik Oct 12 '18 at 10:57
  • @SamVarshavchik the purpose of using idioms is to be consistent and familiar with the reader's expectations. Your advice seems to suggest that code which is obscure, unfamiliar, and unidiomatic is perfectly acceptable so long as it passes tests. I disagree. – Ross Bencina Oct 12 '18 at 11:13
  • 2
    Why exactly is using the simplest implementation of relevance? Put the implementation details in an inner namespace and no user of `has_my_tag` will ever have to care about it... – Max Langhof Oct 12 '18 at 11:38
  • 1
    Your version would require a name change, as you require also that `my_tag` is `void`. – Jarod42 Oct 12 '18 at 12:53
  • 3
    I would call it detect idiom (even if there are several different implementation (as [std::experimental::is_detected](https://en.cppreference.com/w/cpp/experimental/is_detected))) – Jarod42 Oct 12 '18 at 12:55

2 Answers2

7

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

rmawatson
  • 1,909
  • 12
  • 20
  • 2
    I have to say that the way the specialization interacts here (and in the OP) can be unintuitive. If I understand correctly, an instantiation of the template will deduce `has_my_tag` by the `false_type` version's template signature, then substitute `T` into both versions and finally match the `true_type` version due to it being more specialized. Did I get that right? – Max Langhof Oct 12 '18 at 14:22
  • 2
    Yes, sounds like you got it right. It is simply that when instantiating has_my_tag it uses the default value, yeilding has_my_tag. The default is only relevant when instanciating. When templates and specializations are considered, the default value can be ignored, and you are left with (primary template) and a specialization . The is obviously more specialized for these types and thus chosen - like this https://ideone.com/LA6Plr - It's a shame that something actually so simple is made look so unintuitive and complicated with c++. – rmawatson Oct 12 '18 at 14:36
  • Ok, good! I agree that the main confusion stems from the `false_type` template signature being "touched" even when you get the `true_type` one in the end. This of course always happens with every "base case + specializations" templates, but it's not something you'd usually have to consider to understand which version is picked. – Max Langhof Oct 12 '18 at 14:59
3

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:

  1. It can be extended without changing anything about the type you are tagging.

  2. It can be extended using the using tag=void; or using tag=int; that your system works with.

  3. At SFINAE point of use, it is just another compile-time bool. So your existing SFINAE patterns work with it.

  4. 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.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524