Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does template parameter unpacking sometimes not work for std::function?

I encountered a problem. When I use something like std::function<A(Fs...)> it doesn't work, but std::function<A(Fs..., B)> does work. This is under Clang 8.0; none of it works under GCC. Here is the example:

#include <functional>
template<typename A, typename B, typename ...Fs>
void func_tmpl1(std::function<A(Fs..., B)> callable)
{
}
template<typename A, typename ...Fs>
void func_tmpl2(std::function<A(Fs...)> callable)
{
}
class Cls1{};
void func0(std::function<void(float, Cls1)> callable)
{

}

int main()
{
    std::function<void(float, Cls1)> f1 = [](float a, Cls1 b){};
    func0(f1);
    func0([](float a, Cls1 b){});
    func_tmpl1<void, Cls1, float>(f1); // fails in GCC
    func_tmpl2<void, float, Cls1>(f1);

    func_tmpl1<void, Cls1, float>( // fails in GCC
        [](float a, Cls1 b)
        {

        }
    );
    func_tmpl2<void, float, Cls1>( // fails in both
        [](float a, Cls1 b)
        {}
    );

    return 0;
}

On Godbolt, we can see GCC always fails, but Clang only fails at the last function call. Can anyone explain what's happening here?

like image 557
Wang Avatar asked Apr 17 '20 22:04

Wang


2 Answers

For convenience, let's call the three failed calls in your code #1, #2 and #3.

The problem is, when template arguments corresponding to a template parameter pack are explicitly specified, does the template parameter pack still participates in template argument deduction, and if it does, does deduction fail makes the whole call ill-formed?

From [temp.arg.explicit]/9:

Template argument deduction can extend the sequence of template arguments corresponding to a template parameter pack, even when the sequence contains explicitly specified template arguments.

We can infer that the template argument deduction should still be performed.

In the declaration of func_tmpl1, std::function<A(Fs..., B)> is a non-deduced context ([temp.deduct.type]/9: "If the template argument list of P contains a pack expansion that is not the last template argument, the entire template argument list is a non-deduced context."), so template argument deduction for Fs should be ignored and #1 and #2 are both well-formed. There is a GCC bug report.

For #3, template argument deduction obviously fails (std::function<A(Fs...)> vs a lambda type), but does deduction fail really make the code ill-formed? In my opinion, the standard is unclear about this, and there is a related issue. From the response of CWG, #3 is indeed ill-formed.

like image 167
xskxzr Avatar answered Oct 21 '22 22:10

xskxzr


That looks like a compiler bug; the compiler tries template argument deduction when all the arguments are already explicitly specified and hence no deduction is necessary. Or maybe the bug is with substitution, which should succeed.

According to the standard, it is possible to explicitly specify variadic pack arguments. See an example in [temp.arg.explicit]/5:

template<class ... Args> void f2();
void g() {
  f2<char, short, int, long>(); // OK
}

When all template arguments are known, the compiler is supposed to simply instantiate the template and be done with it; overload resolution then proceeds normally.

To work around the issue we can disable template argument deduction by introducing a non-deduced context. For example like this:

template<typename T> using no_deduce = typename std::common_type<T>::type;

template<typename A, typename B, typename ...Fs>
void func_tmpl1(no_deduce<std::function<A(Fs..., B)>> callable)
{
}

template<typename A, typename ...Fs>
void func_tmpl2(no_deduce<std::function<A(Fs...)>> callable)
{
}

(The ::type here is a dependent type and becomes a non-deduced context)

Now it compiles fine in g++ and clang++. link to coliru


Having said that, note that std::function is primarily meant for type erasure and is a costly abstraction as it incurs an extra indirection at run time and is a heavy object to pass around since it tries to store a copy of any possible functor while avoiding heap allocation (which often still takes place - then it's a large empty object plus a heap allocation).

Since your functions are already templates, you don't really need type erasure; it is easier and more efficient to just take callable as a template argument.

template<typename Func>
void func_tmpl(Func callable) // that's all
{
}

Or, if you have to differentiate by callable arguments, can use some SFINAE:

#include <functional>
class Cls1{};

template<typename A, typename B, typename ...Fs, typename Func,
    typename = std::enable_if_t<std::is_invocable_r_v<A, Func, Fs..., B> > >
void func_tmpl1(Func callable)
{
}
template<typename A, typename B, typename ...Fs, typename Func,
    typename = std::enable_if_t<std::is_invocable_r_v<A, Func, B, Fs...> > >
void func_tmpl2(Func callable)
{
}
void func0(std::function<void(float, Cls1)> callable)
{
}

int main()
{
    std::function<void(float, Cls1)> f1 = [](float a, Cls1 b){};
    func0(f1); // func0 is not a template - so it requires type erasure
    func0([](float a, Cls1 b){});
    func_tmpl1<void, Cls1, float>(f1); // #1 OK
    func_tmpl2<void, float, Cls1>(f1); // #2 OK

    func_tmpl1<void, Cls1, float>([](float a, Cls1 b) {}); // #3 OK
    func_tmpl2<void, float, Cls1>([](float a, Cls1 b) {}); // #4 OK

    return 0;
}

link to coliru

like image 33
rustyx Avatar answered Oct 21 '22 23:10

rustyx