当前位置: 首页 > article >正文

C++ 的衰退复制(decay-copy)

目录

1.什么是衰退复制(decay-copy)

1.1.推导规则

1.2.LWG issue 929

1.3.想象中的 decay_copy

2.decay-copy 与 auto

2.1.为什么引入衰退复制

2.2. 成为 C++ 23 的语言特性

3.应用场景

4.总结


1.什么是衰退复制(decay-copy)

1.1.推导规则

        要理解为什么 C++ 会有衰退复制(decay-copy)这个需求,需要了解一下 LWG issue 929 问题的提出,但是要理解这个 issue,最好先复习一下 C++ 模板参数的推导规则。我们按照模板的形参类型进行以下分类,大致可分为三类(不讨论 auto 的推导):

形参类型基本规则

非转发引用类型的引用 :          template<typename T>

void f(T& param);

f(expr);

忽略实参表达式中的引用,将实参表达式 expr 与 param 做模式匹配,确定 T 的类型

转发引用:

template<typename T>

void f(T&& param);

f(expr);

如果 expr 是类型 E 的左值,则 T 被推导为 E&,经过引用折叠后将 param 的类型生成为 E&。对于其他情况,按照普通引用的规则推导(前面一条)。

传值参数:

template<typename T>

void f(T param);

f(expr);

如果 expr 的类型是引用类型,则忽略引用;

如果 expr 表达式含有 CV 限定,则忽略 CV 限定;

然后与 param 做模式匹配,确定 T 的类型。

重点:类型退化

我们不讨论 auto 的推导规则,也不考虑形参表达式与实参表达式的模式匹配的细节,上表是类型推导的基本规则,其中重点是传值参数的类型退化。退化主要有两点,就是数组类型会在推导前退化成指针,函数类型也会在推导前退化成函数指针。当形参类型是引用类型的时候,不会发生退化,这就是为什么如果我们希望推导类型是数组类型,就只能使用引用类型的原因。

1.2.LWG issue 929

        在制定 C++ 11 标准的过程中,C++ 标准委员会的标准库工作组(Library Working Group)收到一个 Issue,就是这个:LWG issue 929 [资料 1]。C++ 11 新增了对并发(concurrency support library)的支持,但是在使用普通函数构造线程的时候会遇到一些麻烦,比如这段代码:

void f(const char*);
std::thread t(f,"hello");

        std::thread 的构造函数使用右值引用传递可调用物和可调用物的参数,并且要求可调用物对应的参数支持可复制构造(CopyConstructible)特性。这样的约束过于严格,限制是使用普通函数作为可调用物的可能性。因为在 std::thread 的构造函数(函数模板)参数推导过程中,因为参数类型使用了右值引用,则绑定的参数类型会被推导为函数的引用类型,而不是退化为函数指针。 于此同时,对参数类型使用右值引用,也阻止了从数组到指针的退化。由于数组不可复制,不可移动,所以结果就是阻止了数组作为可调用物的参数使用的可能性。在这个例子中,字符串字面量会被推导为 const char[6] 类型,显然与 const char * 是不匹配的,这就也阻止了在这种情况下使用字符串字面量的可能。

        如果将 std::thread 的构造函数的参数改成传值类型,数组可以退化成指针,就消除了要求数组具备可复制构造的矛盾。但是传值参数会引起参数传递的效率问题,可见问题没有这么简单。

1.3.想象中的 decay_copy

        既然不能简单使用传值参数,那么可不可以在参数推导的时候做点手动退化呢?当然可以,LWG issue 929 建议引入一个类似这样的 decay_copy() 函数:

template<typename T> 
typename decay<T>::type decay_copy(T&& v);

以便得到一个 v 的右值副本,用于右值引用类型的传递。有了 decay_copy() 函数,你可以将标准库中 std::thread 的构造函数大致理解为这样的实现:

template <class Function, class Arg>
thread(Function&& f, Arg&& arg) {
    std::invoke(std::decay_copy(std::forward<Function>(f)),
                 std::decay_copy(std::forward<Arg>(arg)));
}

        decay_copy() 函数可以将参数拷贝或移动到参数类型的退化版本(的副本)上,与此同时带来的后果就是,当你真的需要传递引用的时候,你需要用 C++ 的引用包装器std::ref 和 std::cref来对抗这种退化。

        好吧,必需得承认,上面的 decay_copy() 函数是我们的想象,C++ 只是引入了衰退复制的概念,但是并没有在库中添加这个函数。至于为什么引入这个概念,以及最后到底怎么实现了这个概念,请继续看下去。

2.decay-copy 与 auto

2.1.为什么引入衰退复制

        C++ 11 引入衰退复制的目的仅仅是为了满足并发库的特殊兴趣爱好吗?当然不是,衰退复制其实是一种具体使用场景的需求,一句话描述这个场景就是:当你需要传递一个对象的副本(拷贝)给一个操作,但是这个操作只接受对象的引用。在这种情况下,如果我们传递的是左值类型的副本,就必需小心维护这个副本的生命周期,否则很可能会发生引用悬挂,如果传递的是右值副本,则可以通过绑定到右值引用而延长生命周期,避免引用悬挂,会相对比较安全。

        根据第 1 节的介绍,我们其实已经知道,所谓的衰退复制,其实就是得到对象的一个纯右值副本。LWG issue 929 建议增加一个 decay_copy() 函数,但是并没有被 C++ 标准采纳。以下是很多资料中都提到的这个函数的具体实现:

template<class T>
constexpr std::decay_t<T> decay_copy(T&& v) noexcept(
    std::is_nothrow_convertible_v<T, std::decay_t<T>>) {
    return std::forward<T>(v);
}

很多情况下,我们可以显式使用 decay_copy() 函数,达到我们的目的。比如这个例子:

void ProcessFoo(Foo&& f);

Foo foo{ 42 };

ProcessFoo(foo); //错误,foo 是左值
ProcessFoo(std::move(foo)); // 可以,但是慎重
ProcessFoo(decay_copy(foo)); //OK,foo 还活着

        在这个例子中,使用 move 可以满足函数调用的要求,但是要慎重,在某些场合, foo 可能会真的被移走,这可能不是我们希望的结果。使用 decay_copy() 就放心多了,通过函数返回值得到一个纯右值副本,也不需要关心它的生命周期维护问题。

        衰退复制的思想在线程库、ranges 库大量使用,在一些异步调用的过程中,以及变量需要被延迟绑定的场合,也会需要这个概念。

2.2. 成为 C++ 23 的语言特性

为什么标准库不要 decay_copy() 函数?上面给了这样一个例子:

class A {
    int x;

public:
    A();

    auto run() {
        f(A(*this));           // ok
        f(auto(*this));        // ok as proposed
        f(decay_copy(*this));  // ill-formed
    }

protected:
    A(const A&);
};

        要拿到一个对象的副本,需要能直接访问对象的拷贝构造函数或移动构造函数,对于这个例子,因为 decay_copy() 函数不是 A 的友元函数,无法访问 A 的构造函数,这就限制了 decay_copy() 函数的使用,毕竟,标准库也不可能把这个函数声明成任何对象的友元。

        从 C++ 11 开始,auto 关键字就一直是个多才多艺的家伙,从自动推导类型占位符到 C++ 20 的模板化 lambda 表达式,auto 关键字的出镜率极高。尽管如此,还有两个表达形式没有被用到,那就是独立的 auto(x) 和 auto{x} 表达式。既然闲着,就拉来加个班吧,于是,用 auto 做纯右值副本,成了 C++ 23 的语言特性。用 auto(x) 或 auto{x}  代替 decay_copy() 函数,不仅仅是查找替换这么简单,auto 有更好的性能。 decay_copy() 函数无论如何都会产生一个对象副本,但是 auto(x) 更智能一点,如果 x 本身就是一个纯右值的话,auto(x) 不做任何操作,不产生开销。对于上一节的 ProcessFoo() 函数,用 auto(x) 代替 decay_copy() 函数:

ProcessFoo(auto(foo)); // OK
ProcessFoo(auto{foo}); // OK

        可能有人会想,已知对象类型是 T ,直接 T() 或 T{} 构造一个对象副本不行吗?为什么还要增加这个语言特性?首先直接构造对象副本存在与 decay_copy() 函数一样的问题,那就是要解决拷贝构造函数或移动构造函数的可访问性,还有就是无论如何都产生副本的操作,在 x 本身是右值的情况下有点多余,这是性能问题。其次是在一些泛型编程或模板库中,T 的具体类型不是那么具体,比如这个例子:

void pop_front_alike(Container auto& x) {
    std::erase(x.begin(), x.end(), auto(x.front()));
}

auto(x.front()) 可以轻松得到一个 x.front() 的右值副本,如果尝试直接调用构造函数,就会麻烦很多:

void pop_front_alike(Container auto& x) {
    using T = std::decay_t<decltype(x.front())>;
    std::erase(x.begin(), x.end(), T(x.front()));
}

        首先要用 decltype 得到 x.front() 返回值的类型,然后还要手动退化一下,因为得到的类型可能是引用类型,或者有 CV 限定。这么做不如 auto 语言特性来的直接,并且如果容器 Container 的 front() 返回类型本身就是右值,这里用 auto 就不会产生任何开销。

3.应用场景

        1) 模板参数推导
        在模板编程中,当模板函数的参数类型与传入的实参类型不匹配时,编译器会根据一系列推导规则来确定模板参数的具体类型。这个过程中可能会发生衰退复制,特别是当实参是数组或函数类型时,它们可能会被推导为相应的指针类型。

C++之std::decay

        2) 并发编程中的线程构造
        在C++11及以后的标准中,引入了并发编程的支持,包括线程(std::thread)等。当使用std::thread构造函数创建线程时,需要传递一个可调用对象(如函数、函数对象、lambda表达式等)及其参数。如果传递的参数类型与std::thread构造函数期望的参数类型不匹配,则可能会发生衰退复制。

std::thread使用及实现原理精讲(全)-CSDN博客

4.总结

        C++中的衰退复制是一个与类型推导和参数传递相关的复杂过程。在编写C++代码时,需要仔细考虑类型匹配和参数传递方式,以避免不必要的衰退复制和潜在的类型错误。

        从 C++ 23 开始,标准库中原来使用的类似 decay_copy() 函数那样的表达形式,都被 auto(x) 取代,在某些情况下得到了性能提升。


http://www.kler.cn/a/442503.html

相关文章:

  • 你喜欢用什么编辑器?
  • Mysql--实战篇--数据库设计(范式和反范式,数据表设计原则)
  • 数据结构-线性表
  • 如何通过openssl生成.crt和.key
  • 【力扣Hot100】滑动窗口
  • 浅谈ArcGIS的地理处理(GP)服务之历史、现状和未来
  • 画一颗随机数
  • Firefox 基本设置备忘
  • cursor的composer功能
  • Mac/Linux 快速部署TiDB
  • Uniapp图片跨域解决
  • Python Tkinter 弹窗美化指南
  • 不坑盒子2024.1218更新了,模板库上线、一键添加拼音、一键翻译……支持Word、Excel、PPT、WPS
  • Vite 系列课程|1课程道路,2什么是构建工具
  • 汽车服务管理系统(源码+数据库+报告)
  • 京准电钟国产信创:北斗授时服务器的应用及详细介绍
  • Face to face
  • aac怎么转为mp3?操作起来很简单的几种aac转mp3的方法
  • 大屏开源项目go-view二次开发2----半环形控件(C#)
  • uniapp 微信小程序 功能入口
  • JVM内存泄漏之ThreadLocal详解
  • uni-app设置页面不存在时跳转到指定页面
  • 超越 RAG 基础:AI 应用的高级策略
  • [LeetCode] 746.使用最小花费爬楼梯
  • ASP.NET |日常开发中连接Mysql数据库增删改查详解
  • Springboot实现本地文件上传、下载、在线预览