Search code examples
c++templatesc++20sfinaec++-concepts

Concept to define type A to be equal to type B, if B exists


I want TagOrInt<T> to be equal to T::Tag if T has a member type Tag, and equal to int otherwise. Like:

template <class T> using TagOrint = typename T::Tag; // if this is valid
template <class T> using TagOrInt = int;             // otherwise

I can do this using SFINAE and a helper struct:

#include <type_traits>
template<class T, class = void>
struct TagOrIntHelper {
  using type = int;
};
template<class T>
struct TagOrIntHelper<T, std::void_t<typename T::Tag> > {
  using type = T::Tag;
};
template<class T>
using TagOrInt = TagOrIntHelper<T>::type;

// Test case
struct X { using Tag = float; };
struct Y { };
static_assert(std::is_same_v<TagOrInt<X>, float>);
static_assert(std::is_same_v<TagOrInt<Y>, int>);

This works, but is there a way to do it without SFINAE and a helper struct? I feel that there should be a C++20 way that uses concepts. Or is there an even a simpler way in C++17?

This was my attempt with concepts, but it does not work:

#include <type_traits>
template<class T>
concept HasTag = requires { (typename T::Tag *)nullptr; };
template<class T>
using TagOrInt = std::conditional_t<HasTag<T>, typename T::Tag, int>;

// Test case
struct Y { };
static_assert(std::is_same_v<TagOrInt<Y>, int>);

The instantiation of Tag_or_int<Y> fails to compile:

concept.cc:5:7: error: no type named ‘Tag’ in ‘struct Y’

So apparently std::conditional_t requires that both branches compile.

(It was suggested that this question is a duplicate of How to deduce the type of template argument based on some conditions and return information about that type . However, the two questions are unrelated:

  • This question regards how to define a type to an expression that depends on a template argument, if that expression is valid in a particular instantiation, and to some other "default" type otherwise. Further, I want to achieve this using concepts rather than the known SFINAE solution.
  • That question regards how to initialize a member variable to a value that is computed using a type-dependent algorithm. The answers given there are not answers to the question here, and vice versa. So I would like to request that this question is re-opened, especially since both the answers here are interesting and instructive. Thank you!)

Solution

  • You could do it this way:

    template <class T>
    using TagOrint = decltype([]{
        if constexpr (requires { typename T::Tag; }) {
            return std::type_identity<typename T::Tag>();
        } else {
            return std::type_identity<T>();
        }
    }())::type;
    

    Basically - the lambda gives you a place where you can if constexpr on that constraint. But the lambda can't return a type, it can only return a value - so we instead return some kind of std::type_identity<T>. And then we need to decltype(...)::type to actually pull the T out.