0

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.

Rahul Bhobe
  • 4,165
  • 4
  • 17
  • 32
  • https://stackoverflow.com/a/55490839/4181011 – Simon Kraemer Aug 26 '21 at 09:04
  • 1
    C++17 changed the start of object lifetimes. It's no longer copy-elision, there simply aren't intermediate objects to be copied. – Caleth Aug 26 '21 at 09:15
  • "Under the following circumstances, the compilers are required to omit the copy and move construction of class objects, even if the copy/move constructor and the destructor have observable side-effects. The objects are constructed directly into the storage where they would otherwise be copied/moved to." – Caleth Aug 26 '21 at 09:30
  • 4
    That's the thing -- it isn't an optimization. It's a change in *where execution happens*. If you're trying to use `source_location` for something other than identifying where execution happens, you should edit your question to clarify what specific feature you're trying to achieve. – Sneftel Aug 26 '21 at 09:55
  • The object `o3` in `main` is constructed in `getCopy1`, it is the same object as `ret`. Ditto `o4` and the un-named prvalue in `getCopy2` – Caleth Aug 26 '21 at 13:35
  • @Sneftel Added and highlighted more details. – Rahul Bhobe Sep 01 '21 at 06:59
  • How about changing only `getCopy1` and `getCopy2` but not `o1...o4`? – n. m. could be an AI Sep 01 '21 at 07:38
  • @n.1.8e9-where's-my-sharem. That would be impractical too. Too many to modify. This is a template container. – Rahul Bhobe Sep 01 '21 at 07:48
  • You only need to change the type, not anything inside. – n. m. could be an AI Sep 01 '21 at 07:50
  • @n.1.8e9-where's-my-sharem. Not quire sure what you mean. Each instance of a function returning a `MyClass` object has its own logic. There are very many of these functions. Did your suggestion assume that `getCopy1()` constructs a clone of an existing object like so `getCopy1(o1)`? Maybe my function name is a misnomer. I should have named it `getObject1()/getObject2()`. – Rahul Bhobe Sep 01 '21 at 07:58
  • I mean that you only change the return type of each function without changing the logic. `MyClass` will take care of the mismatch. – n. m. could be an AI Sep 01 '21 at 08:10
  • That won't work here either, sorry. Too many of these. – Rahul Bhobe Sep 01 '21 at 08:11
  • Why do you need it to lie about `o3` and `o4` being constructed in `main`? There isn't a copy because the standard says there isn't a copy. That's how C++ works now, it is no longer an optimisation. – Caleth Sep 01 '21 at 08:17
  • Good question @Caleth. I will update my post to include that info. – Rahul Bhobe Sep 01 '21 at 08:26

1 Answers1

1

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.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243