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

C++17及以上能否针对特定类型禁用强制拷贝消除特性

问题背景

在C++17及以上版本中,强制拷贝消除(guaranteed copy elision)特性支持在链式函数调用中返回不可移动对象,逐层传递至最终调用方,示例代码如下:

struct NonMoveable {
  NonMoveable() = default;
  NonMoveable(NonMoveable&&) = delete;
};

NonMoveable Foo() { return NonMoveable(); }
NonMoveable Bar() { return Foo(); }
NonMoveable Baz() { return Bar(); }

NonMoveable non_moveable = Baz();

核心问题:是否存在实现技巧,能够针对特定类型禁用guaranteed copy elision,使得上述示例中Bar、Baz这类透传从其他函数获取的NonMoveable对象的函数无法通过编译?是否同时禁用Foo函数无强制要求。


需求场景

该需求源自协程库开发场景:

  • 开发协程库时约定,若函数返回Task类型,则该函数所有引用类型参数的生命周期必须持续到任务就绪,即对应任务的co_await表达式完成求值为止。
  • 若协程直接在另一协程中调用,该机制可正常工作:已将Task类型设计为不可移动且按值接收,除了立即对其执行co_await外无法执行其他操作,调用子协程时传入的所有临时对象的生命周期会持续到co_await表达式求值完成。

现存安全漏洞

当前存在隐蔽性极强的安全问题:开发者可编写类似上述Bar、Baz的函数,使用return Foo()而非co_return co_await Foo()的写法。

  • 若Foo调用时传入的参数包含临时对象,co_return co_await Foo(...)写法是安全的
  • 但return Foo(...)写法会因临时对象提前销毁引发悬空引用问题

举个典型触发场景:std::function<Task(SomeObj)>内部包含return语句,可正常绑定接收const SomeObj&参数的lambda,调用时会将其按值形参的引用传入lambda,而该形参会在lambda挂起时被销毁,直接触发未定义行为。

当前亟需更优雅的方案规避该问题,短期目标是让存在风险的写法直接编译失败,便于定位用户代码中的问题。推测该需求可能无法通过标准语法实现,但希望确认是否存在未考虑到的实现技巧。


可行实现方案

完全可以通过标准C++语法实现该需求,不需要依赖编译器扩展,核心是利用「拷贝消除生效时仍会检查构造函数访问权限」的标准规则,给Task类型增加权限控制即可:

#include <type_traits>

struct Task;
template<typename T>
constexpr bool is_task_v = std::is_same_v<std::remove_cvref_t<T>, Task>;

struct Task {
    // 保留原有不可移动、不可拷贝约束
    Task() = default;
    Task(Task&&) = delete;
    Task(const Task&) = delete;

private:
    // 转发拦截标记:仅授权合法构造场景访问
    struct ConstructToken {
        // 授权两类合法场景:1. 直接在当前函数构造Task返回的顶层函数;2. co_await子协程的包装逻辑
        friend Task make_task(auto&&... args);
        friend Task await_resolve(auto&& awaitable);
        ConstructToken() = default;
    };

public:
    // 仅持有合法ConstructToken的场景可以构造Task
    explicit Task(ConstructToken) /* 原有Task初始化逻辑 */ {}
};

// 直接构造Task的工具函数,对应示例中Foo这类直接返回Task的合法场景
Task make_task(auto&&... args) {
    return Task(Task::ConstructToken{});
}

// co_await子协程的包装,处理等待逻辑后合法返回Task
Task await_resolve(auto&& awaitable) {
    // 原有co_await等待、生命周期保活逻辑
    return Task(Task::ConstructToken{});
}

// 合法:Foo直接构造Task返回,有ConstructToken访问权限,可正常编译
Task Foo() {
    return make_task(/* 任务参数 */);
}

// 非法:Bar透传Foo返回的Task,没有ConstructToken访问权限,编译直接报错
Task Bar() {
    return Foo();
}

// 非法:Baz透传Bar返回的Task,同样编译失败
Task Baz() {
    return Bar();
}

// 合法:协程内使用co_return co_await写法,走await_resolve逻辑,可正常编译
Task valid_coro() {
    co_return co_await Foo();
}

实现原理

  • C++标准明确规定:即使拷贝消除被强制生效、不会实际调用拷贝/移动构造函数,编译器仍必须检查对应构造路径的访问权限,否则程序非良构。
  • 透传返回的场景下,外层函数没有ConstructToken的私有构造权限,哪怕返回值是纯右值触发拷贝消除,也会因为无法访问合法构造函数直接编译报错,刚好拦截return Foo()这类风险写法。
  • 两类合法场景(直接构造Task返回、co_await等待后返回)因为提前被授予了ConstructToken的友元权限,完全不受影响,可以正常编译运行。
  • 该方案可以100%拦截透传返回的风险写法,包括std::function包装lambda这类隐蔽场景,只要存在非法透传就会在编译期抛出错误,不需要额外的静态检查工具,零运行时开销。

内容的提问来源于stack exchange,提问作者jacobsa

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 10:27:21