Search code examples
c++templatesc++20variadic-templatesperfect-forwarding

How to perfect forward variadic template args with default argument std::source_location?


I want to track where the creation of an object occurs using std::source_location. The following is a simplified example without the extra code to account for copy and movement of a Tracked object.

#include <iostream>
#include <memory>
#include <source_location>

template <typename Underlying>
struct Tracked 
{
    Underlying val;
    std::source_location loc;
};

template <typename Underlying, typename T>
auto makeTracked(T&& t, std::source_location loc = std::source_location::current()) 
{
    // make_unique is a simplification, the actual code uses something more complicated
    // the important thing is that there is an extra function call here
    return std::make_unique<Tracked<Underlying>>(Underlying{ std::forward<T>(t) }, std::move(loc));
}

int main() 
{
    auto p = makeTracked<std::string>("abc");
    std::cout << p->val << "@" << p->loc.line() << std::endl;
    // Expected usage:
    // auto p1 = makeTracked<std::tuple<int, int, int>>(3, 4, 5);
}

The code currently works, but only for types whose constructor takes exactly one parameter. I want to change makeTracked to be variadic, but then it conflicts with the default argument for loc.

I have checked the related question, and none of the answers solve my issue completely.

This answer changes the function to a struct, which is fine in the original question because we don't care about the type of the return value (it is void in the original question), but that is not the case here. Even if we were fine with the return type being a makeTracked object instead of a unique_ptr, the problem still arises as how to specify the Underlying template parameter as the original answer uses deduction guide, and we can't have partial deduction guides that only deduce the T (or Ts... in the expected usage) but still allow us to specify the Underlying explicitly.

This answer requires that you know the type of the first argument (it cannot be deduced), which does not apply here.

The first alternative in this answer that manually expands all the cases for 0 argument, 1 argument, 2 arguments... is actually closest to what is working, however it requires that we know the maximal number of arguments in advance.

Is there a way to make the commented out part of "Expected usage" work for an arbitrary number of arguments, which also perfectly forwards all the arguments?


Solution

  • I'm not quite sure why @JeJo brought concepts into his answer. Here is a slightly less verbose solution:

    template<typename Underlying>
    struct MakeTracked {
        std::source_location loc = std::source_location::current();
    
        template<typename... Args>
        constexpr auto operator()(Args&&... args) const {
            return std::make_unique<Tracked<Underlying>>(
                Underlying{ std::forward<Args>(args)... },
                loc
            );
        }
    };
    
    // ...
    
    auto p1 = MakeTracked<std::tuple<int, int, int>>{}(3, 4, 5);
    

    Demo