Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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!)
like image 637
Sven Sandberg Avatar asked Sep 13 '25 03:09

Sven Sandberg


2 Answers

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.

like image 95
Barry Avatar answered Sep 14 '25 18:09

Barry


Here we do SFINAE based testing of a set of traits:

template<class T, class=void, template<class...>class... Zs>
struct first_valid {};

template<class T, template<class...>class Z, template<class...>class... Zs>
struct first_valid<T, std::void_t<Z<T>>, Z, Zs...>
{
  using type=Z<T>;
};
template<class T, class V, template<class...>class Z, template<class...>class... Zs>
struct first_valid<T, V, Z, Zs...>:first_valid<T,V, Zs...>{};

template<class T, template<class...>class... Zs>
using first_valid_t = typename first_valid<T, void, Zs...>::type;

now we can:

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

struct Bob {
  using Tag = Bob;
};

template<class T>
using test = first_valid_t<T, TagOrint, TagOrInt>;

int main() {
  test<Bob> b = Bob{};
  test<double> d = 7;
}

first_valid_t<T, templates...> finds the first valid template to apply and returns that type.

like image 35
Yakk - Adam Nevraumont Avatar answered Sep 14 '25 18:09

Yakk - Adam Nevraumont