如何强制非聚合类的成员进行原地构造?
强制非聚合类成员原地构造的解决方法
有时我希望强制类(尤其是非聚合类)的某些成员进行原地构造,要么是为了提升效率,要么是为了确保行为的确定性。但我一直没能实现这个目标,下面通过尝试实现作用域守卫(scope guard)来具体说明问题。
我希望隐藏_func和_exec成员,同时禁用拷贝/移动构造与赋值,所以不能用聚合类。我最初写了一个简单实现:
template<class F> class [[nodiscard]] at_scope_exit { F _func; bool _exec = true; public: at_scope_exit(F&& f)noexcept(std::is_nothrow_move_constructible_v<F>):_func{ std::move(f) } {} void disable()noexcept { _exec = false; } at_scope_exit(const at_scope_exit&) = delete; at_scope_exit& operator=(const at_scope_exit&) = delete; ~at_scope_exit() { if (_exec) { _func(); } } }; void useit() { int i = 1; at_scope_exit atexit{ [&] {i--; } }; }
这个实现大部分情况能正常运行,但无法保证成员原地构造。比如下面这个例子:
struct bad_closure { ~bad_closure() { std::cout << "~bad_closure()"; } void operator()() { std::cout << "operator()"; } }; void problem() { at_scope_exit atexit{ bad_closure{} }; //output: ~bad_closure()operator()~bad_closure() }
先不管实际中该不该用bad_closure这种结构,这种先构造再移动的行为让我很不舒服(这个场景下编译器似乎不允许做拷贝消除)。怎么才能消除这种可能性?应该如何修改at_scope_exit,让下面的代码不会出现额外的拷贝或移动?
void expected() { struct foo { ~foo() { std::cout << "~foo()"; } }; int i = 1; at_scope_exit atexit{ [&i,f = foo{}] { i = 0; } }; /* required output: ~foo() */ }
解决方案:原地构造成员变量,避免临时对象移动
核心思路是直接在_func的内存位置上构造F对象,完全跳过临时对象的创建和移动步骤。以下是两种可行的实现方式:
方法1:完美转发模板构造函数(C++11及以上)
修改构造函数,让它接受任意参数并完美转发给_func的构造,直接在成员位置初始化:
#include <utility> template<class F> class [[nodiscard]] at_scope_exit { F _func; bool _exec = true; public: // 模板构造函数,接受构造F的所有参数并完美转发 template<typename... Args> explicit at_scope_exit(Args&&... args) noexcept(std::is_nothrow_constructible_v<F, Args&&...>) : _func(std::forward<Args>(args)...) {} void disable() noexcept { _exec = false; } // 彻底禁用拷贝和移动 at_scope_exit(const at_scope_exit&) = delete; at_scope_exit& operator=(const at_scope_exit&) = delete; at_scope_exit(at_scope_exit&&) = delete; at_scope_exit& operator=(at_scope_exit&&) = delete; ~at_scope_exit() { if (_exec) { _func(); } } }; // C++17推导指引,让编译器自动推导F类型 template<typename F> at_scope_exit(F) -> at_scope_exit<F>;
方法2:显式原地构造(兼容C++11,更直观)
如果需要更明确地控制构造过程,可以用placement new直接在_func的内存地址上构造对象:
#include <utility> #include <type_traits> #include <memory> template<class F> class [[nodiscard]] at_scope_exit { F _func; bool _exec = true; public: // 禁用所有拷贝/移动操作 at_scope_exit(const at_scope_exit&) = delete; at_scope_exit& operator=(const at_scope_exit&) = delete; at_scope_exit(at_scope_exit&&) = delete; at_scope_exit& operator=(at_scope_exit&&) = delete; // 原地构造F的构造函数 template<typename... Args> explicit at_scope_exit(Args&&... args) noexcept(std::is_nothrow_constructible_v<F, Args&&...>) { // C++20可用std::construct_at,C++11用placement new ::new (static_cast<void*>(std::addressof(_func))) F(std::forward<Args>(args)...); } void disable() noexcept { _exec = false; } ~at_scope_exit() { if (_exec) { _func(); } // 成员变量会自动析构,无需手动调用 } }; // C++17推导指引 template<typename F> at_scope_exit(F) -> at_scope_exit<F>;
效果验证
修改后测试problem()函数,输出变为operator()~bad_closure(),说明只构造了一次bad_closure,没有临时对象的额外析构。
测试expected()函数,输出符合要求的~foo(),闭包中的foo对象只会在作用域结束时随闭包一起销毁,没有额外的拷贝或移动操作。
关键细节
- 原来的构造函数接受
F&&会触发临时对象的创建,即使移动构造无开销,也可能因编译器无法消除移动导致额外行为; - 新的构造函数直接在成员位置构造
F,完全避免临时对象; - 必须同时禁用移动构造和赋值,防止意外的移动操作。
内容的提问来源于stack exchange,提问作者fir las
相关产品推荐
相关产品推荐

