如何为使用std::source_location的构造函数禁用复制消除?
问题背景
我正在为产品中广泛使用的模板类添加插桩逻辑,当前开发环境为VS 2019 (16.10.4),编译选项为/std:c++17。std::source_location特性非常适配我要解决的任务,但我的编译器不支持std::source_location和std::experimental::source_location,因此我参考相关资料自行实现了该功能。我的目标是在特殊构建版本中修改类的构造函数和成员后运行测试,修改类本身不改变其用法,其余代码无需调整。
上述实现大部分情况下编译运行正常,但遇到了copy elision(复制消除)问题,几乎让使用std::source_location的目的落空。我希望获取的source_location是调用方定义变量的位置,而非实际创建对象的位置。
这个问题在gcc -std=c++20环境下开启或关闭-fno-elide-constructors选项时也可复现。
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) { } };
调用示例:
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; }
实际输出:
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()
预期输出:
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()
现有可行但不实用的解决方案:
- 方案1(显式传参):
MyClass o3(getCopy1(), std::source_location::current()); MyClass o4(getCopy2(), std::source_location::current());
- 方案2(使用std::move):
MyClass o3(std::move(getCopy1())); MyClass o4(std::move(getCopy2()));
上述两种方案都能得到预期结果,但需要修改大量上层调用代码,违背了使用std::source_location降低改造成本的初衷。我可以接受不破坏原有调用方式的MyClass.h/cpp修改,或者编译器选项调整,该插桩版本仅用于测试,不用于生产,产品当前基于VS 2019 (16.10.4)、/std:c++17编译,请问有什么合适的解决方案?
解决方案
方案1:编译选项调整(零代码修改)
针对VS2019环境,直接在测试构建的编译参数中添加/Zc:copyElision-即可。该参数会禁用C++17标准要求的强制复制消除特性,你的原有MyClass实现不需要做任何修改,即可得到预期的调用侧位置信息。该方案完全符合测试版本的使用场景,不需要修改任何业务代码,仅需要单独调整测试构建的编译配置即可,不要在生产构建中开启该选项。
方案2:隐式转换代理实现(零调用侧修改)
如果不希望修改编译选项,可以通过轻量代理类利用隐式转换的时机捕获调用侧位置,仅需要调整MyClass的头文件定义,上层调用代码完全不需要改动:
class MyClass; // 轻量代理类,用于捕获转换位置 class MyClassProxy { private: MyClass&& tmp_obj; std::source_location loc; friend class MyClass; public: MyClassProxy(MyClass&& obj, std::source_location l = std::source_location::current()) : tmp_obj(std::move(obj)), loc(l) {} }; class MyClass { private: int m_a = 0; std::source_location m_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) { } friend class MyClassProxy; public: MyClass(std::source_location location = std::source_location::current()) : m_location(location) { } // 新增代理构造函数,使用调用侧捕获的位置 MyClass(MyClassProxy proxy) : MyClass(std::move(proxy.tmp_obj), proxy.loc) {} // 原有getLocationInfo等逻辑保持不变 std::string getLocationInfo() const { // 原有实现不变 } };
该方案同时兼容GCC和VS编译器,不需要调整任何上层调用代码即可得到预期输出。
内容的提问来源于stack exchange,提问作者Rahul Bhobe

