C++ variadic template function parameter with default value

11,489

Solution 1

No, packs must be last.

But you can fake it. You can detect what the last type in a pack is. If it is SomeSpecialType, you can run your func. If it isn't SomeSpecialType, you can recursively call yourself with your arguments forwarded and fromNum(5) appended.

If you want to be fancy, this check can be done at compile time (ie, a different overload) using SFINAE techniques. But that probably isn't worth the hassle, considering that the "run-time" check will be constant on a given overload, and hence will almost certainly be optimized out, and SFINAE shouldn't be used lightly.

This doesn't give you the signature you want, but it gives you the behavior you want. You'll have to explain the intended signature in comments.

Something like this, after you remove typos and the like:

// extract the last type in a pack.  The last type in a pack with no elements is
// not a type:
template<typename... Ts>
struct last_type {};
template<typename T0>
struct last_type<T0> {
  typedef T0 type;
};
template<typename T0, typename T1, typename... Ts>
struct last_type<T0, T1, Ts...>:last_type<T1, Ts...> {};

// using aliases, because typename spam sucks:
template<typename Ts...>
using LastType = typename last_type<Ts...>::type;
template<bool b, typename T=void>
using EnableIf = typename std::enable_if<b, T>::type;
template<typename T>
using Decay = typename std::decay<T>::type;

// the case where the last argument is SomeSpecialType:
template<
  typename... Args,
  typename=EnableIf<
    std::is_same<
      Decay<LastType<Args...>>,
      SomeSpecialType
    >::value
  >
void func( Args&&... args ) {
  // code
}

// the case where there is no SomeSpecialType last:    
template<
  typename... Args,
  typename=EnableIf<
    !std::is_same<
      typename std::decay<LastType<Args...>>::type,
      SomeSpecialType
    >::value
  >
void func( Args&&... args ) {
  func( std::forward<Args>(args)..., std::move(static_cast<SomeSpecialType>(fromNum(5))) );
}

// the 0-arg case, because both of the above require that there be an actual
// last type:
void func() {
  func( std::move(static_cast<SomeSpecialType>(fromNum(5))) );
}

or something much like that.

Solution 2

Another approach would be to pass variadic arguments through a tuple.

template <class... Args>
void func (std::tuple<Args...> t, SomeSpecialType num = fromNum(5))
{
  // don't forget to move t when you use it for the last time
}

Pros : interface is much simpler, overloading and adding default valued arguments is quite easy.

Cons : caller has to manually wrap arguments in a std::make_tuple or std::forward_as_tuple call. Also, you'll probably have to resort to std::index_sequence tricks to implement the function.

Solution 3

This is coming a bit late, but in C++17 you can do it with std::tuple and it would be quite nice overall. This is an expansion to @xavlours 's answer:

template <class... Args>
void func (std::tuple<Args&&...> t, SomeSpecialType num = fromNum(5))
{
    // std::apply from C++17 allows you to iterate over the tuple with ease
    // this just prints them one by one, you want to do other operations i presume
    std::apply([](auto&&... args) {((std::cout << args << '\n'), ...);}, t);
}

Then, make a simple function to prepare them:

template<typename... Args>
std::tuple<Args&&...> MULTI_ARGS(Args&&... args) {
    return std::tuple<Args&&...>(args...);
}

Now you can call the function like this:

func(MULTI_ARGS(str1, int1, str2, str3, int3)); // default parameter used
func(MULTI_ARGS(str1, int1, str2));  // default parameter used
func(MULTI_ARGS(str1, int1, str2, str3, int3, otherStuff), fromNum(10)); // custom value instead of default

Disclaimer: I came across this question as I was designing a logger and wanted to have a default parameter which contains std::source_location::current() and as far as I was able to find, this is the only way that ensures the caller's information is passed accurately. Making a function wrapper will change the source_location information to represent the wrapper instead of the original caller.

Solution 4

Since C++17 there is way to work around this limitation, by using class template argument deduction and user-defined deduction guides.

This is espactialy useful for C++20 std::source_location.

Here is C++17 demo:

#include <iostream>

int defaultValueGenerator()
{
    static int c = 0;
    return ++c;
}

template <typename... Ts>
struct debug
{    
    debug(Ts&&... ts, int c = defaultValueGenerator())
    {
        std::cout << c << " : ";
        ((std::cout << std::forward<Ts>(ts) << " "), ...);
        std::cout << std::endl;
    }
};

template <typename... Ts>
debug(Ts&&...args) -> debug<Ts...>;

void test()
{
    debug();
    debug(9);
    debug<>(9);
}

int main()
{
    debug(5, 'A', 3.14f, "foo");
    test();
    debug("bar", 123, 2.72);
}

Live demo

Demo with source_location (should be available since C++20, but still for compilers it is experimental).

Share:
11,489

Related videos on Youtube

cfa45ca55111016ee9269f0a52e771
Author by

cfa45ca55111016ee9269f0a52e771

Updated on June 03, 2022

Comments

  • cfa45ca55111016ee9269f0a52e771
    cfa45ca55111016ee9269f0a52e771 almost 2 years

    I have a function which takes one parameter with a default value. Now I also want it to take a variable number of parameters and forward them to some other function. Function parameters with default value have to be last, so... can I put that parameter after the variadic pack and the compiler will detect whether I'm supplying it or not when calling the function?

    (Assuming the pack doesn't contain the type of that one last parameter. If necessary, we can assume that, because that type is generally not supposed to be known to the user, otherwise it's considered as wrong usage of my interface anyway....)

    template <class... Args>
    void func (Args&&... args, SomeSpecialType num = fromNum(5))
    {
    }
    
  • cfa45ca55111016ee9269f0a52e771
    cfa45ca55111016ee9269f0a52e771 over 11 years
    So it's like a workaround, it's a different signature but the same behavior... I see. Actually I was planning to remove that parameter in the future, so maybe it's not worth the effort (and the signature would be confusing). Can you show me a simple example?
  • Yakk - Adam Nevraumont
    Yakk - Adam Nevraumont over 11 years
    @fr33domlover I sketched out the design. Hasn't been compiled, let alone debugged, but the fundamentals should be there.
  • cfa45ca55111016ee9269f0a52e771
    cfa45ca55111016ee9269f0a52e771 over 11 years
    Thanks, I'll try it if I don't just decide to remove the single parameter. It looks complicated, and the signature isn't kept, so it may not be worth the trouble... anyway thanks
  • Yakk - Adam Nevraumont
    Yakk - Adam Nevraumont about 9 years
    @Ninetainedo awesome! Can you provide evidence? I mean, they don't have to be last in the template argument list, but in the function argument list, they (effectively) have to be last (presuming a free function with deduced argument packs)?
  • Telokis
    Telokis about 9 years
    Ok, I misunderstood the sentence, my bad, then. :x
  • Yakk - Adam Nevraumont
    Yakk - Adam Nevraumont over 3 years
    Another approach like this is template<class...Args> auto func( Args&&... args ) { return [&]( SomeSpecialType num = fromNum(5) ) { /* code */ }; }, called like func( first, arguments, here )( extra_optional_argument ).
  • xavlours
    xavlours over 3 years
    @Yakk-AdamNevraumont: I like the idea, why not make a dedicated answer ? The drawback is that in the default case, you need to call func(a1, a2, a3)() right ?
  • Marek R
    Marek R over 2 years
    Idea is good, but implementation is wrong: godbolt.org/z/hehE48zqq