5

I am trying to have a kind of "invoke" function with an optional argument at the end:

template <typename... T>
void foo(void func(T...), T... args, int opt = 0)
{
    func(args...);
}

void bar(int, int);

int main()
{
    foo(&bar, 1, 2, 3);
}

I would have expected this to work, since the parameter pack can be deduced from the first argument, but clearly the compilers have different ideas:

Clang for example gives:

<source>:11:5: error: no matching function for call to 'foo'
    foo(&bar, 1, 2, 3);
    ^~~
<source>:2:6: note: candidate template ignored: deduced packs of different lengths for parameter 'T' (<int, int> vs. <>)
void foo(void func(T...), T... args, int opt = 0)
     ^
1 errors generated.
Compiler returned: 1

Why is it deducing a list of length 0? Can I force it to ignore args for the purposes of deduction? Or more generally, how can I make this work?

Drew Dormann
  • 59,987
  • 13
  • 123
  • 180
Lukas Lang
  • 400
  • 2
  • 11
  • Your first error is because you didn't expand `args` in the call. It needs to be `func(args...);` – Ryan Haining Jun 30 '22 at 17:50
  • The default value for opt is a red herring, you can get the error [even without the =0](https://wandbox.org/permlink/Wiz21kCPhBDfH3GX) – Ryan Haining Jun 30 '22 at 17:52
  • Related: https://stackoverflow.com/q/35701888 – Ch3steR Jun 30 '22 at 18:02
  • @RyanHaining Thanks, that's what I get for not double checking my MWE... should be fixed now – Lukas Lang Jun 30 '22 at 18:22
  • @Ch3steR Thanks! I have looked at that before, but re-reading it now makes it clear that "can be deduced from other arguments" is not a valid way out from the pack not being the last argument. I wonder why it was done like this... – Lukas Lang Jun 30 '22 at 18:25
  • @JeJo Thanks! It seems the current version does the right thing by accident, but reading up on it suggests your version is the correct one – Lukas Lang Jun 30 '22 at 18:29
  • @RyanHaining Thanks for pointing that out - it does indeed make the origin of the error more clear. (I'll leave it in the question however as it is relevant for a solution, since the usual "move the argument before the pack" doesn't work here) – Lukas Lang Jun 30 '22 at 18:30

2 Answers2

3

You could make it overloaded instead of having an optional argument. You'd need to move the "optional" to before the parameter pack though.

The second overload would then just forward the arguments to the first, with the "default" parameter set.

#include <iostream>

template <typename... T>
void foo(void(func)(T...), int opt, T... args)
{
    std::cout << opt << '\n';
    func(args...);
}

template <typename... T>
void foo(void(func)(T...), T... args)
{
    return foo(func, 0, args...);    // forwards with the default set
}

void bar(int, int) {}

int main()
{
    foo(&bar, 1, 2);      // prints 0
    foo(&bar, 3, 1, 2);   // prints 3
}

You might want to move the optional all the way to the first position to let the function and its parameters be together. It's a matter of taste.


Another option could be to exclude the optional parameter and only have the parameter pack and to extract the optional if it's present or use the default value if it's not. This requires that you restrict the signature of func to match the function you aim to call.

#include <iostream>
#include <tuple>

template <class... T>
void foo(void func(int, int), T&&... args) {
    int opt = [](T... args) {
        if constexpr (sizeof...(T) > 2) return std::get<2>(std::tuple{args...});
        else return 0;  // use the default
    }(args...);

    std::cout << opt << '\n';

    [&func](int a, int b, auto&&...) { func(a, b); }(args...);
}

void bar(int, int) {}

int main() {
    foo(&bar, 1, 2);     // prints 0
    foo(&bar, 1, 2, 3);  // prints 3
}

Building on the second version but giving a lot more freedom, you could introduce a separate parameter pack for func. If that pack has the same size as pack of arguments supplied, you need to pick a default value for opt. If it on the other hand contains more arguments than needed for the function, you can select which one of the extra arguments that should be used for opt. In the example below, I just picked the first extra parameter.

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

// a helper to split a tuple in two:
template <class... T, size_t... L, size_t... R>
auto split_tuple(std::tuple<T...> t,
                     std::index_sequence<L...>,
                     std::index_sequence<R...>)
{
    return std::pair{
               std::forward_as_tuple(std::get<L>(t)...),
               std::forward_as_tuple(std::get<R+sizeof...(L)>(t)...)
           };
}

template <class... A, class... T>
void foo(void func(A...), T&&... args) {
    static_assert(sizeof...(T) >= sizeof...(A));
    
    // separate the needed function arguments from the rest:
    auto[func_args, rest] = 
        split_tuple(std::forward_as_tuple(std::forward<T>(args)...),
                    std::make_index_sequence<sizeof...(A)>{},
                    std::make_index_sequence<sizeof...(T)-sizeof...(A)>{});

    int opt = [](auto&& rest) {
        // if `rest` contains anything, pick the first one for `opt`
        if constexpr(sizeof...(T) > sizeof...(A)) return std::get<0>(rest);
        else return 0; // otherwise return a default value
    }(rest);

    std::cout << opt << '\n';

    std::apply(func, func_args);
}

void bar(int a, int b) {
    std::cout << a << ',' << b << '\n';
}

int main() {
    foo(&bar, 1, 2);        // prints 0 then 1,2
    foo(&bar, 1, 2, 3, 4);  // prints 3 then 1,2
}
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
2

how can I make this work?

You can put the function arguments in a std::tuple, to make them distinct from your optional parameter.

C++17 provides std::apply to unpack the tuple parameters for you.

#include <tuple>

template <typename... T>
void foo(void func(T...), std::tuple<T...> args, int opt = 0)
{
    std::apply( func, args );
}

void bar(int, int);

int main()
{
    foo(&bar, {1, 2}, 3);
//       args ^^^^^^
}
Drew Dormann
  • 59,987
  • 13
  • 123
  • 180