You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何强制非聚合类的成员进行原地构造?

强制非聚合类成员原地构造的解决方法

有时我希望强制类(尤其是非聚合类)的某些成员进行原地构造,要么是为了提升效率,要么是为了确保行为的确定性。但我一直没能实现这个目标,下面通过尝试实现作用域守卫(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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 01:33:02