C++ variadic template function parameter with default value
Asked Answered
T

4

29

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))
{
}
Termination answered 11/2, 2013 at 2:45 Comment(0)
W
23

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.

Warram answered 11/2, 2013 at 2:48 Comment(5)
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?Termination
@fr33domlover I sketched out the design. Hasn't been compiled, let alone debugged, but the fundamentals should be there.Warram
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 thanksTermination
@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)?Warram
Ok, I misunderstood the sentence, my bad, then. :xKymric
M
9

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.

Molybdenum answered 28/9, 2015 at 7:32 Comment(2)
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 ).Warram
@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 ?Molybdenum
G
9

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

Gigantes answered 11/2, 2022 at 15:41 Comment(1)
A good article going into additional detail on this technique: cppstories.com/2021/non-terminal-variadic-argsIntermolecular
N
1

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.

Northwest answered 26/12, 2021 at 7:33 Comment(1)
Idea is good, but implementation is wrong: godbolt.org/z/hehE48zqqGigantes

© 2022 - 2024 — McMap. All rights reserved.