15

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?

Weijun Zhou
  • 746
  • 1
  • 7
  • 25

2 Answers2

10

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

Nelfeal
  • 12,593
  • 1
  • 20
  • 39
  • 3
    Oh, nice. How did I miss the obvious one! I was blindly over-thinking and came up with over-engineering. +1. – JeJo Jul 09 '23 at 10:56
  • 2
    This is a good solution. Unfortunately some programmers will twist themselves into knots trying to find a way to avoid the need for the extra braces, instead of doing real work. – Brian Bi Jul 09 '23 at 17:30
  • 1
    @BrianBi What do you mean by that? Can we do without the extra {}? – LernerCpp Jul 09 '23 at 20:52
  • 3
    @LernerCpp I have no idea, but I'm sure someone will try. – Brian Bi Jul 09 '23 at 22:15
9

I want to change makeTracked to be variadic, but then it conflicts with the default argument for loc.

You can maybe provide, two step function call using functors:

#include <concepts>  // std::convertible_to

template<typename T = std::source_location> 
    requires std::convertible_to<T, std::source_location>
struct MakeTracked 
{
    T loc{ std::source_location::current() };

    template <typename Underlying, typename... Args>
    constexpr auto operator()(Args&&... args) const
    {
        return std::make_unique<Tracked<Underlying>>(
            Underlying{ std::forward<Args>(args)... },
            loc
        );
    }
};

You would call it (a bit verbosely):

auto p1 = MakeTracked<>{}.operator()<std::tuple<int, int, int>>(3, 4, 5);

See a live demo in godbolt.org

JeJo
  • 30,635
  • 6
  • 49
  • 88
  • 2
    Thank you very much for the answer. I had something similar in mind but haven't tried actually implementing it because as you said, it makes the call a bit more verbose. Anyway, I will wait for some more time and if no one comes up with a way that avoids the verbosity, I will accept your answer. – Weijun Zhou Jul 09 '23 at 09:08