Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Simplest implementation of "lightweight type categorization idiom"?

Tags:

c++

c++11

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.

like image 986
Ross Bencina Avatar asked Oct 12 '18 10:10

Ross Bencina


2 Answers

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

like image 54
rmawatson Avatar answered Sep 21 '22 00:09

rmawatson


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.

like image 33
Yakk - Adam Nevraumont Avatar answered Sep 20 '22 00:09

Yakk - Adam Nevraumont