How to force SFINAE to choose the second definition of structure?

Viewed 98

Before that I want to tell that I have tried to implement is_assignable on my own. There is no need to show me another examples - I have already seen some implementation.

I would like to fix my solution thanks to you (if it's possible, of course) that'll work out.

So, here is my code:

#include <iostream>
#include <type_traits>
#include <utility>

template<typename LambdaT>
struct is_valid_construction {
    is_valid_construction(LambdaT) {}
    
    typedef typename LambdaT lambda_prototype;

    template<typename ValueTypeT, typename ExprTypeT = decltype(std::declval<lambda_prototype>()(std::declval<ValueTypeT>()))>
    struct evaluate {
        evaluate(ValueTypeT val) {
            std::cout << "Right!";
        }
        typedef typename std::true_type value;
    };

    template<typename ValueTypeT> //The compiler ignores this definition
    struct evaluate<ValueTypeT, decltype(std::declval<lambda_prototype>()(std::declval<int>()))> {
        evaluate(ValueTypeT val) {
            std::cout << "Nope";
        }
        typedef typename std::false_type value;
    };

    template<typename ValueTypeT>
    void print_value(ValueTypeT val) {
        evaluate evaluation(val);
    }
};

struct ForTest {};

int main() {
    is_valid_construction is_assignable([](auto x) -> decltype(x = x) { });
    is_valid_construction is_less_comparable([](auto x) -> decltype(x < x) {});
    is_valid_construction is_more_comparable([](auto x) -> decltype(x > x) {});

    is_assignable.print_value(int{});
    is_less_comparable.print_value(char{});
    is_more_comparable.print_value(ForTest{});

    return 0;
}

As you can see I am trying to define template structure within template structure. So, I excepted that if the invocation (with declval) of this lambda-expression with parameter of this type (rougly, in terms of substitution) is failed, then SFINAE goes further and should see that the second template definition could be convenient for instantiation. I am asking how could I fix my template structure and its default parameter to push SFINAE use the second definition?

3 Answers

SFINAE can be used in order to direct the compiler to choose a particular function overload, or a particular partial specialization of a class template. In the first case, substitution failures remove declarations from the overload set and in the second case, substitution failures remove the partial specialization declarations from consideration (causing either the primary template to be used, or a different partial specialization for which substitution succeeds).

But what you are trying to do here is backward: you have a situation where the primary template is potentially subject to substitution error, and you provide a partial specialization as an alternative. This can never work. Partial specialization matching begins after the template argument list to the primary template is fully known, therefore if a substitution error occurs in the primary template's template argument list, no specializations can be considered.

For example if we have

template <typename T, typename U = some_metafunction_of_T>
struct S;

template <typename T>
struct S<T, T>;

then the instantiation process of S<int> will first evaluate U for the primary template, and then, only once T and U are both known, the compiler can determine whether or not they are the same (which would allow the partial specialization to be used). If a substitution error occurs while computing U, the question of whether the partial specialization applies cannot even be asked.

To fix your code, you would have to switch the two definitions of evaluate. The primary template would have to be the "fallback", and the partial specialization would have to be potentially subject to substitution error.

as @Brian said, you should put the requirements at the primary template if the requirements are for all specializations, and put other requirements for each specialization at their own declarations:

template<typename T, typename = std::void_t</* global requirements */>>
struct S;
template<typename T>
struct S<T, std::void_t</* requirements for this specialization */>>;

and if you want one of specialization is prior to others, you can add its negative requirements to other specializations:

template<typename T, typename = std::void_t</* global requirements */>>
struct S;
template<typename T>
struct S<T, std::void_t<std::enable_if_t</* conditions for this specialization */>>>;
template<typename T>
struct S<T, std::void_t<std::enable_if_t<!/* conditions for the former specialization */>, /* requirements for this specialization */>>;

for your example, it should be like this:

template<typename Lambda>
struct is_valid_construction{
    template<typename T, typename = void>
    struct helper : std::false_type{};
    template<typename T>
    struct helper<T, std::void_t<decltype(std::declval<Lambda>()(std::declval<T>()))>> : std::true_type{};

    template<typename V, typename = void>
    struct evaluate;
    template<typename V>
    struct evaluate<V, std::enable_if_t<helper<V>::value>>;
    template<typename V>
    struct evaluate<V, std::void_t<std::enable_if_t<!helper<V>::value>, decltype(std::declval<Lambda>()(std::declval<int>()))>>;
};

by the way, you can use std::is_invocable to simplify this code:

template<typename Lambda>
struct is_valid_construction{
    template<typename V, typename = void>
    struct evaluate;
    template<typename V>
    struct evaluate<V, std::enable_if_t<std::is_invocable_v<Lambda, V>>>;
    template<typename V>
    struct evaluate<V, std::enable_if_t<!std::is_invocable_v<Lambda, V> && std::is_invocable_v<Lambda, int>>>;
};

Thanks to @RedFog and @Brian I could complete my code and I have got the such result:

#include <iostream>
#include <type_traits>
#include <utility>

template<typename LambdaT>
struct is_valid_construction {
    is_valid_construction(LambdaT) {}

    typedef LambdaT lambda_prototype;

    template<class ValueT, class = void>
    struct is_void_t_deducable : std::false_type {};

    template<class ValueT>
    struct is_void_t_deducable<ValueT,
                               std::void_t<decltype(std::declval<lambda_prototype>()(std::declval<ValueT>()))>> : std::true_type {};

    template<class ValueT>
    bool is_valid_for(ValueT value) {
        if constexpr (is_void_t_deducable<ValueT>::value)
            return true;
        else
            return false;
    }
};

struct ForTest {};

int main() {
    is_valid_construction is_assignable([](auto x) -> decltype(x * x) { });
    std::cout << is_assignable.is_valid_for(0) << std::endl;
    std::cout << is_assignable.is_valid_for(ForTest{});

    return 0;
}

As they both said, that when I had declared template parameter like that:

template<typename ValueTypeT, typename ExprTypeT = decltype(std::declval<lambda_prototype>()(std::declval<ValueTypeT>()))>

the compiler didn't understand what a default value should the second template parameter assign and since both declarations are incompatible.

I am new one in template programming and I can try to explain the solution as simple as possible:

The second template parameter is (if to say not strictly!) should be void. So, the compiler can instantiate the template with second void parameter in two ways by means of first declaration or second declaration.

(It should be said that std::void_t<TemplateParam> becomes void if TemplateParam is well!)

  • If an instantiation with the second declaration is well, then the second template parameter is void.

  • If an instantiation with the first declaration is well, then the second template parameter is void.

So, we should help compiler to deduce both structures with the second template parameter void. When it tries to instantiate is_valid_for(ForTest{}) first of all it tries to deduce std::void_t<decltype(std::declval<lambda_prototype>()(std::declval<ValueT>()))> but gets substitution error. However, nothing prevents to deduce the second template parameter void in another way and the compilers takes the first declaration.

P.S. I know that this explanation is not good but it may help dummies like me!

Related