简体   繁体   中英

C++ variadic template function parameter with default value

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))
{
}

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.

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.

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

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM