SFINAE doesn't work in recursive function
Asked Answered
C

2

7

Let's create currying function.

template <typename TFunc, typename TArg>
class CurryT
{
public:
    CurryT(const TFunc &func, const TArg &arg)
      : func(func), arg(arg )
        {}

    template <typename... TArgs>
        decltype(auto) operator()(TArgs ...args) const
            { return func(arg, args...); }

private:
    TFunc func;
    TArg  arg ;
};

template <typename TFunc, typename TArg>
    CurryT<decay_t<TFunc>, remove_cv_t<TArg>>
        Curry(const TFunc &func, const TArg &arg)
            { return {func, arg}; }

And function that decouple function to single argument functions:

// If single argument function (F(int)).
template <typename F>
    static auto Decouple(const F &f, enable_if_t<is_invocable_v<F, int>> * = nullptr)
    {
        return f;
    }

// If multiple arguments function (F(int, int, ...)).
template <typename F>
    static auto Decouple(const F &f, enable_if_t<!is_invocable_v<F, int>> * = nullptr)
    {
        return [f](int v) { return Decouple( Curry(f, v) ); };
    }

Everything works fine if 2 arguments function is passed:

auto f1 = Decouple(
    [](int a, int b)
        { std::cout << a << " " << b << std::endl; }
);
f1(3)(4); // Outputs 3 4

But if I add more arguments

auto f2 = Decouple(
    [](int a, int b, int c)
        { std::cout << a << " " << b << " " << c << std::endl; }
);
f(5)(6)(7);

The compilation breaks: https://coliru.stacked-crooked.com/a/10c6dba670d17ffa

main.cpp: In instantiation of 'decltype(auto) CurryT<TFunc, TArg>::operator()(TArgs ...) const [with TArgs = {int}; TFunc = main()::<lambda(int, int, int)>; TArg = int]':

main.cpp:17:26: error: no match for call to '(const main()::<lambda(int, int, int)>) (const int&, int&)'
   17 |             { return func(arg, args...); }

It breaks in instantiation of std::is_invocable.

Since debugging the standard library is hard, I created simple versions of standard type traits classes:

template <typename F> true_type  check(const F &, decltype( declval<F>()(1) )* );
template <typename F> false_type check(const F &, ...);

template <typename F>
    struct invocable_with_int : decltype(check(declval<F>(), nullptr))
        {};

template <typename F>
    inline constexpr bool invocable_with_int_v = invocable_with_int<F>::value;

template<bool B>
    struct my_enable_if {};

template<>
    struct my_enable_if<true>
        { using type = void; };

template <bool B>
    using my_enable_if_t = typename my_enable_if<B>::type;

The problem remains the same https://coliru.stacked-crooked.com/a/722a2041600799b0:

main.cpp:29:73:   required by substitution of 'template<class F> std::true_type check(const F&, decltype (declval<F>()(1))*) [with F = CurryT<main()::<lambda(int, int, int)>, int>]'

It tries to resolve calling to this function:

template <typename F> true_type  check(const F &, decltype( declval<F>()(1) )* );

But decltype (declval<F>()(1))*) fails. But shouldn't this function be removed from overload resolution because template substitution fails? It works when Decouple is called first time. But when it is called second time the SFINAE seems to be disabled, and the first failure of template substitution gives a compilation error. Are there some limitation on secondary SFINAE? Why calling template function recursively doesn't work?

The problem is reproduced in GCC and Clang. So it is not a compiler bug.

Chane answered 9/5, 2022 at 13:53 Comment(0)
M
10

Your operator() overload is completely unconstrained and therefore claims to be callable with any set of arguments. Only declarations, not definitions, are inspected to determine which function to call in overload resolution. If substitution into the definition then fails, SFINAE does not apply.

So, constrain your operator() to require TFunc to be callable with TArg and TArgs... as arguments.

For example:

template <typename... TArgs>
auto operator()(TArgs ...args) const -> decltype(func(arg, args...))
Morphophonemics answered 9/5, 2022 at 14:36 Comment(8)
Wow! Thank you. I spent so much time trying to resolve that issue. But what I still don't understand why my non-working solution works when passing the function with 2 arguments. I should fail on the first call to Decouple, but it doesn't.Chane
@Chane With two arguments, you only try to test std::is_invocable<F, int> with F a CurryT where it really does expect one int argument. There is only an issue when you have a CurryT instance that expects to be called with two or more arguments, in which case std::is_invocable<F, int> still gives true because of the lack of constraints.Morphophonemics
Oh, understand. At the first call (when there are 2 arguments) it is not CurryT, it is lambda. And std::is_invocable_v works fine with lambdas. At the second call it is CarryT. But then it works because in that specific case CarryT works with single int argument. Thank you.Chane
PS I added const to CurryT for not allowing to change arg when calling func (if it accepts the first argument by reference and modifies it). In other case it would not be classic currying. I also ommited && and std::forward to make example simple.Chane
I resolved the issue by simply using trailing return type with decltype. In this case trying to call function with single argument becomes part of declaration. coliru.stacked-crooked.com/a/46c680dfc72a1b72Chane
@Chane Yes, that is a better approach and avoids having to consider the type qualifiers. For some reason it doesn't seem to work with your custom type-traits version, but I'll just assume that this is just a bug in your custom trait implementations.Morphophonemics
It works. The only issue was that I used wrong variable name (f instead of f2).Chane
@Chane Ah yes, and I forgot to adjust the declaration order in the class.Morphophonemics
L
0

For me it is strange that your CurryT::operator() accepts unknown number of arguments.

Since aim is to have a functions which accept only one argument I expected that this function will accept only one argument.

IMO depending what kind of function CurryT holds CurryT::operator() should return a different type: return type of starting function or another version of CurryT.

Here is my approach using std::bind_front from C++20:

namespace detail {
template <typename TFunc>
class CurryT
{
public:
    explicit CurryT(TFunc f) : mF(std::move(f))
    {}

    template<typename T>
    auto get(T&& x, int = 0) -> decltype(std::declval<TFunc>()(x)) {
        return mF(x);
    }

    template<typename T>
    auto get(T&& x, char) {
        return CurryT<decltype(std::bind_front(mF, std::forward<T>(x)))>{
            std::bind_front(mF, std::forward<T>(x))
        };
    }

    template<typename T>
    auto operator()(T&& x)
    {
        return this->get(std::forward<T>(x), 1);
    }
private:
     TFunc mF;
};
}

template<typename F>
auto Decouple(F&& f)
{
    return detail::CurryT<std::decay_t<F>>{std::forward<F>(f)};
}

https://godbolt.org/z/eW9r4Y6Ea

Note with this approach integer argument is not forced like in your solution.

Lucilucia answered 9/5, 2022 at 15:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.