简体   繁体   中英

SFINAE doesn't work in recursive function

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.

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...))

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM