How to disable copy elision for constructors with std::source_location?

347 views Asked by At

I am trying to add instrumentation to a widely used template class in my product. I am currently on VS 2019 (16.10.4) with /std:c++17. The new feature of std::source_location is a great addition for the kind of task I am interested in solving. While std::source_location and std::experimental::source_location are not available on my compiler, I built my own based on this answer. My goal is make changes to the class constructor/members in a special build and run tests. The changes to the class itself does not change its usage, so rest of the code remains same.

This all compiles and works great - mostly. Except, I run into the copy elision which almost beats the purpose of using std::source_location. I'd like my source_location to be the location at which the caller variable is defined instead of where the actual object is created.

This problem can be demonstrated on gcc -std=c++20 with and without -fno-elide-constructors as well. See simplified version of my minimal reproducible godbolt sample.

MyClass:

class MyClass
{
private:
    int m_a = 0;
    std::source_location m_location;

public:
    MyClass(std::source_location location = std::source_location::current())
    : m_location(location)
    {
    }
    MyClass(const MyClass &other, std::source_location location = std::source_location::current())
    : m_a(other.m_a)
    , m_location(location)
    {
    }
    MyClass(MyClass &&other, std::source_location location = std::source_location::current())
    : m_a(other.m_a)
    , m_location(location)
    {
    }
};

Usage:


MyClass getCopy1()
{
    MyClass ret;
    return ret;
}

MyClass getCopy2()
{
    return MyClass();
}

int main()
{
    MyClass o1;
    MyClass o2(o1);
    MyClass o3(getCopy1());
    MyClass o4(getCopy2());

    std::cout << "o1: " << o1.getLocationInfo() << std::endl;
    std::cout << "o2: " << o2.getLocationInfo() << std::endl;
    std::cout << "o3: " << o3.getLocationInfo() << std::endl;
    std::cout << "o4: " << o4.getLocationInfo() << std::endl;

    return 0;
}

Actual Output:

o1: /app/example.cpp(56:13) int main()
o2: /app/example.cpp(57:18) int main()
o3: /app/example.cpp(46:12) MyClass getCopy1()
o4: /app/example.cpp(51:20) MyClass getCopy2()

Expected Output:

o1: /app/example.cpp(56:13) int main()
o2: /app/example.cpp(57:18) int main()
o3: /app/example.cpp(58:26) int main()
o4: /app/example.cpp(59:26) int main()

Workaround 1 (explicit argument):

    MyClass o3(getCopy1(), std::source_location::current());
    MyClass o4(getCopy2(), std::source_location::current());

Workaround 2 (std::move):

    MyClass o3(std::move(getCopy1()));
    MyClass o4(std::move(getCopy2()));

While any of these workarounds above give me the results I desire, they are impractical. These will require me to change several instances of usage code and will beat the purpose of using something like std::source_location. Any changes to the MyClass.h/cpp that does not break its usage or any changes to the compiler flags is fair game. I intend to have a separate instrumented build that will not be used in production. My product currently builds on VS 2019 (16.10.4) with /std:c++17.

1

There are 1 answers

0
n. m. could be an AI On

You can try something like this:

template <int line, std::size_t N, basic_fixed_string<N> file> 
class MyClassImpl {
   public:
      // copy from any instantiation 
      template <int line2, std::size_t N2, basic_fixed_string<N2> file2>
      MyClassImpl(const MyClassImpl<line2, N2, file2>& other) { ... }
      // also move, assignment etc
      ...
};

#define MyClass MyClassImpl<__LINE__, sizeof(__FILE__)-1, __FILE__>

You can take basic_fixed_string from here or roll your own; also feel free to use your implementation of std::source_location instead of the old-style macros if possible. The idea is to make each mention of MyClass a unique type. The template parameters are not used in the class definition, they could be anything, as long as they are unique.

Now there's no longer copy elision when you do

MyClass o3(getCopy1());

because the types are not the same, so there's no copy to be elided.

Of course this doesn't work at all if you use auto:

auto o3 = getCopy1(); // still has copy elision

so this is not a robust solution.