Home > Net >  Optional argument after template parameter pack of supposedly known length
Optional argument after template parameter pack of supposedly known length

Time:07-01

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?

CodePudding user response:

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
}

CodePudding user response:

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 ^^^^^^
}
  • Related