3

Lately i tried to write wrapper arount void_t idiom simple as following:

namespace detail {

template <class Traits, class = void>
struct applicable : std::false_type {};

template <class... Traits>
struct applicable<std::tuple<Traits...>, std::void_t<Traits...>>
    : std::true_type {};

}  // namespace detail

template <class... Traits>
using applicable = detail::applicable<Traits...>;

and something like than on the call side


template <class T>
using call_foo = decltype(std::declval<T>().foo());

template <class T>
using has_foo = applicable<call_foo<T>>;

auto main() -> int {
    std::cout << std::boolalpha << has_foo<std::vector<int>>::value;
}

but insted of expected "false" i get compile-time error:

error: 'class std::vector<int, std::allocator<int> >' has no member named 'foo'
 using has_foo = my::applicable<call_foo<T>>;

What is wrong?

Update: The idea behind using parameter pack in traits is to use this applicable metafunction as follows:

template <class T>
using call_foo = decltype(std::declval<T>().foo());

template <class T>
using call_boo = decltype(std::declval<T>().boo());

template <class T>
using call_bar = decltype(std::declval<T>().bar());

template <class T>
using has_foo_and_boo_and_bar = applicable<call_foo<T>, call_boo<T>, call_bar<T>>;

The key here is not to apply trait onto multiple classes but to apply multiple traits onto one class without the use of std::conjunction.

David
  • 125
  • 8
  • what would be the type of `call_foo` if `std::vector` has no `foo` method? – Ch3steR Apr 05 '22 at 05:07
  • @Ch3steR ill-formed – David Apr 05 '22 at 05:09
  • 1
    [SFINAE](https://en.wikipedia.org/wiki/Substitution_failure_is_not_an_error#:~:text=Substitution%20failure%20is%20not%20an%20error%20(SFINAE)%20refers%20to%20a,to%20describe%20related%20programming%20techniques.) is only applicable when there's an error in template parameters it ignores it and chooses most appropriate instantiation. [`using`](https://en.cppreference.com/w/cpp/language/type_alias) is used to create an alias for existing types. `call_foo` and `has_foo` are aliases any failure in their template parameter gives an error. – Ch3steR Apr 05 '22 at 05:16
  • ok. i've omitted `using`s completely and used trait directly `applicable().foo())>::value`, this also gives compile time error. – David Apr 05 '22 at 05:27
  • 1
    Your substitution has to fail in template instantiation. In `applicable().foo())>::value`, `decltype(std::declval().foo())` is ill-formed, `applicable` is not valid. – Ch3steR Apr 05 '22 at 05:36
  • But why? There is fallback on that case where void_t has to choose the least spesified template. – David Apr 05 '22 at 05:37
  • Think of it this way, `applicable< type_needed >` but `decltype(std::declval().foo())` is not valid. `applicable< not_valid >` would be an error, right? – Ch3steR Apr 05 '22 at 05:37
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/243606/discussion-between-david-and-ch3ster). – David Apr 05 '22 at 05:40
  • @David I am not sure what you want to happen with tuple, can you enlighten me ? – Martin Morterol Apr 05 '22 at 11:29
  • @MartinMorterol I've used it to wrap parameter pack, otherwise it wouldn't be possible to use `class = void` at the end of the declaration – David Apr 05 '22 at 18:25
  • @David Does the answer help? If yes, feel free to upvote. If it answers the question, you can accept it. And if not, feel free to ask an explanation or point out a missing part. – Martin Morterol Apr 08 '22 at 05:23
  • 1
    @MartinMorterol Sorry for being inactive. Thank you your answer helped. – David Apr 09 '22 at 20:21

1 Answers1

1

Something like that :

#include <type_traits>
#include <vector>
using namespace std;


struct Foo{
    int foo() { return 1 ;}
};


// transform an "maybe type" into a classic type traite
// We use T and not Args... so we can have a default type at the end
// we can use a type container (like tuple) but it need some extra boilerplate
template<template<class...> class Traits, class T, class = void>
struct applicable : std::false_type {};

template<template<class...> class Traits, class T>
struct applicable<
        Traits,
        T, 
        std::void_t< Traits<T> >
    > : std::true_type {};


// not an usual type trait, I will call this a "maybe type"
template <class T>
using call_foo = decltype(std::declval<T>().foo());


// creating a type trait with a maybe type
template<class T>
using has_foo_one = applicable<call_foo, T>;

static_assert( has_foo_one<std::vector<int>>::value == false );
static_assert( has_foo_one<Foo>::value == true  );

// we need a way to check multiple type at once
template <
    template<class...> class Traits,
    class... Args
>
inline constexpr bool all_applicable = (applicable<Traits,Args>::value && ...);

static_assert( all_applicable<call_foo,Foo,Foo> == true  );
static_assert( all_applicable<call_foo,Foo,int> == false  );


template<class ... Args>
struct List{};


// if you want the exact same syntaxe
template<
    template<class...> class Traits, // the type traits
    class List,                      // the extra  boilerplate for transforming args... into a single class
    class = void                     // classic SFINAE
    >                   
struct applicable_variadic : std::false_type {};

template<
    template<class...> class Traits,
    class... Args
    >
struct applicable_variadic 
    <Traits,
    List<Args...>, // can be std::tuple, or std::void_t  but have to match line "using has_foo..."
    std::enable_if_t<all_applicable<Traits, Args...> // will be "void" if all args match Traits
    >
> : std::true_type {};

template<class... Args>
using has_foo = applicable_variadic<call_foo, List<Args...>>;

static_assert( has_foo<Foo,Foo>::value == true  );
static_assert( has_foo<Foo>::value == true  );
static_assert( has_foo<Foo,int>::value == false  );

int main() {
   
    return 1;
 }

https://godbolt.org/z/rzqY7G9ed

You probably can write all in one go, but I separate each part. I found it easier to understand when I go back on my code later on.


Note:

In your update you want:

template <class T>
using call_foo = decltype(std::declval<T>().foo());

template <class T>
using call_boo = decltype(std::declval<T>().boo());

template <class T>
using call_bar = decltype(std::declval<T>().bar());

template <class T>
using has_foo_and_boo_and_bar = applicable<call_foo<T>, call_boo<T>, call_bar<T>>;

It's impossible. applicable<int, ERROR_TYPE> will not compile. It's not a "substitution error" it is an error.

You have 2 options (AFAIK)

  • Use boolean logic applicable<traits_foo<T>::value, traits_bar<T>::value>. Note the value. In this case each type trait will tell if a property is respected and applicable will just check that all boolean are true.
  • Pass some template class (so not type_traits<T> but just type_traits) and the type to check and use SFINAE in applicable. That what I have done below.

With the same principle, we can create a "list of template class". On this implementation, we expect a type traits to have a ::value that why I pass has_bar_one and not call_bar

template<template<class...> class... Traits>
struct list_of_template_class{};


template<
    class ListOfTraits,
    class T,                      
    class = void                     
    >                   
struct applicable_X_traits : std::false_type {};

template<
    template<class...> class... Traits ,
    class T
    >
struct applicable_X_traits 
    <list_of_template_class<Traits...>,
    T,
    std::enable_if_t< ( Traits<T>::value && ...) >
> : std::true_type {};


template <class T>
using call_bar = decltype(std::declval<T>().foo());

template<class T>
using has_bar_one = applicable<call_foo, T>;


template<class T>
using has_foo_bar = applicable_X_traits<
                        list_of_template_class<has_bar_one, has_foo_one>,
                        T
                    >;

static_assert(has_foo_bar<Foo>::value == true  );

static_assert(has_foo_bar<int>::value == false  );


struct JustBar {
    void bar() { }
};

static_assert(has_foo_bar<JustBar>::value == false  );

https://godbolt.org/z/K77o3KxTj


Or just use Boost::Hana

// If you have an instance of T you can just do :
auto has_foo_bar_simpler = hana::is_valid([](auto&& p) -> std::void_t<
    decltype(p.foo()), 
    decltype(p.bar())
>{ });


static_assert(has_foo_bar_simpler(1) == false  );
static_assert(has_foo_bar_simpler(JustBar{}) == false  );
static_assert(has_foo_bar_simpler(Foo{}) == true  );

// if not 
template<class T>
constexpr bool has_foo_bar_simpler2 = decltype(has_foo_bar_simpler(std::declval<T>())){};
static_assert(has_foo_bar_simpler2<int> == false  );
static_assert(has_foo_bar_simpler2<JustBar> == false  );
static_assert(has_foo_bar_simpler2<Foo> == true  );

https://godbolt.org/z/aM5YT8a56

Martin Morterol
  • 2,560
  • 1
  • 10
  • 15
  • But now the semantics of `applicable` is different - specialization for `std::tuple` is lost. – Evg Apr 05 '22 at 08:03
  • Indeed, I miss this part. I am not sure what you expect to happen with `std::tuple` ? to check if all type in the tuple are `foo` compatible ? – Martin Morterol Apr 05 '22 at 08:16
  • 1
    I don't have an answer to this question, we should ask OP. `std::tuple` part is somewhat vague. I'm not sure it's really needed, but it's there nevertheless. – Evg Apr 05 '22 at 08:19
  • I used tuple to wrap parameter pack, because wasn't sure where my mistake was, thanks for the answer @Evg – David Apr 05 '22 at 18:04
  • Why `Args` in `appliable` decalration is not parameter pack btw? – David Apr 05 '22 at 18:11
  • It can be something like that https://godbolt.org/z/GosaTefxv – David Apr 05 '22 at 18:12
  • I have updated with a version of `has_foo` variadic and some comment – Martin Morterol Apr 06 '22 at 06:14