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

Modern Effective C++ 条款二十三:理解std::move和std::forward

可以从它们不做的角度什么理解move和forward。

std::move不移动任何东西,std::forward也不转发任何东西。

在运行时,它们不做任何事情。它们不产生任何可执行代码,一字节也没有。 std::movestd::forward仅仅是执行转换(cast)的函数(事实上是函数模板)。

std::move无条件的将它的实参转换为右值,而std::forward只在特定情况满足时下进行转换。从根本上而言,这就是全部内容。

C++11的std::move的示例实现。它并不完全满足标准细则,但是它已经非常接近了。

template<typename T>//在std命名空间
typename remove_reference<T>::type&& move(T&& param){
    using ReturnType = typename remove_reference<T>::type&&;//别名声明,见条款9
    return static_cast<ReturnType>(param);
}

std::move接受一个对象的通用引用(见item24),返回一个指向同对象的引用。该函数返回类型的&&部分表明std::move函数返回的是一个右值引用,但是,如果类型T恰好是一个左值引用,那么T&&将会成为一个左值引用。为了避免如此,type trait (见item 9)std::remove_reference应用到了类型T上,因此确保了&&被正确的应用到了一个不是引用的类型上。这保证了std::move返回的真的是右值引用,因为函数返回的右值引用是右值。因此,std::move将它的实参转换为一个右值。

std::move在C++14中可以被更简单地实现,使用函数返回值类型推导和标准库的模板别名std::remove_reference_tstd::move可以这样写:

template<typename T>
decltype(auto) move(T&& param)          //C++14,仍然在std命名空间
{
    using ReturnType = remove_referece_t<T>&&;
    return static_cast<ReturnType>(param);
}

std::move它只进行转换,不移动任何东西。右值是移动操作的候选者,所以对一个对象使用std::move就是告诉编译器,这个对象很适合被移动。std::move告诉编译器指定可以被移动的对象。

在C++中,std::move 是一个用于将对象转换为右值引用的工具,它通常用来提示编译器可以对对象进行移动语义操作。然而,当涉及到 const 对象时,事情会变得有些复杂。

class Annotation {
public:
//原始版本:使用值传递
    explicit Annotation(std::string text):value(text){}
//修改后的版本:使用 const 引用传递
    explicit Annotation(const std::string& text):value(std::move(text)) { }
private:
    std::string value;
}

Annotation(std::string text) 接受一个 std::string 的副本。这里 text 是一个临时变量,它被复制到 value 中。如果 text 是一个临时变量(例如,通过 new Annotation("Some Text") 创建),那么这个复制可以通过移动语义优化。

修改后的构造函数:

Annotation(const std::string& text) 接受一个 const std::string&,这是一个常量引用。使用 std::move(text) 尝试将 text 转换为右值引用。然而,因为 text 是 const,所以即使使用了 std::move,它仍然是一个 const 右值引用。std::string 的移动构造函数接受非 const 的右值引用 (std::string&&),因此不能被调用。编译器选择调用拷贝构造函数,因为它可以接受 const 右值引用。

结论

(1)不要对 const 对象使用 std::move:如果希望利用移动语义,不应该声明参数为 const。否则,std::move 会静默地退化为拷贝操作。

(2)std::move 不保证移动:std::move 只是将对象转换为右值引用,但并不保证实际的移动会发生。如果对象类型不支持或不允许移动(如 const 对象),则可能会发生拷贝而非移动。

为了确保移动语义,如果确实需要移动对象,应该避免将其声明为 const。如果不需要修改传入的对象,同时又希望保留移动语义的可能性,可以考虑使用非常量引用:

explicit Annotation(std::string&& text) : value(std::move(text)) { }

std::move 与 std::forward 的区别

std::move 是一个无条件的转换,它总是将它的参数转换为右值引用(rvalue reference)。这个操作本身并不移动任何东西;它只是允许对象被移动构造或移动赋值,通常用于当你确定不再需要一个对象,并且希望将其资源转移给另一个对象时。

std::forward 是一个有条件的转换,只有当传递给它的参数是右值时,它才会将参数转换为右值引用。如果参数是左值,则保持其左值属性。std::forward 主要用于模板函数中,以确保参数在转发到其他函数时保留其原始的值类别(左值还是右值)。

std::forward 的典型用法

考虑一个模板函数 logAndProcess,它接收一个通用引用参数 param 并将其传递给 process 函数。process 函数有两个重载版本:一个处理左值引用,另一个处理右值引用。为了正确地调用相应的 process 版本,我们需要使用 std::forward 来保持 param 的原始值类别。

template<typename T>
void logAndProcess(T&& param) {
    // 记录日志
    makeLogEntry("Calling 'process'", std::chrono::system_clock::now());
    // 转发参数到 process
    process(std::forward<T>(param));
}

当 logAndProcess 用左值调用时,param 应该作为左值传递给 process;当用右值调用时,param 应该作为右值传递。std::forward 通过检查 T 是否包含引用类型来决定是否进行转换。

使用 std::move 当你需要明确地进行移动操作。使用 std::forward 在模板中转发参数时,保持参数的原始值类别。std::move 和 std::forward 都是在编译期起作用的,不会在运行时产生额外开销。

在模板函数 logAndProcess 中,如果不使用 std::forward<T>(param) 而直接使用 param,那么无论传递给 logAndProcess 的是左值还是右值,param 都会被视为左值。这是因为函数参数总是左值,即使它是一个通用引用(universal reference)。

template<typename T>
void logAndProcess(T&& param) {
    // 记录日志
    makeLogEntry("Calling 'process'", std::chrono::system_clock::now());
    // 直接使用 param 而不是 std::forward<T>(param)
    process(param);
}

当调用 logAndProcess 时

传递左值:

Widget w;
logAndProcess(w);  // w 是左值

T 将被推导为 Widget&(即 Widget 的左值引用),param 是一个左值引用。因此,process(param) 实际上会调用process(const Widget& lvalArg)。

传递右值

logAndProcess(Widget());  // 临时对象是右值

在这种情况下,T 将被推导为Widget(即 Widget 的非引用类型),param 是一个右值引用。但是,由于 param 作为函数参数,它仍然是一个左值。因此,process(param) 实际上也会调用 process(const Widget& lvalArg),而不是 process(Widget&& rvalArg)。

结论

如果不使用 std::forward,process 函数的重载版本将总是选择处理左值引用的那个版本,即使原始参数是一个右值。如果 process 的右值引用版本可以更高效地处理右值(例如,通过移动语义),那么将失去这种优化。如果 process 的两个重载版本有不同的行为,那么不使用 std::forward 可能会导致错误的行为,因为总是会调用左值引用版本。


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

相关文章:

  • git查看本地库对应的远端库的地址
  • 故障诊断 | Transformer-LSTM组合模型的故障诊断(Matlab)
  • Vue2-从零搭建一个项目(项目基本结构介绍)
  • Git中HEAD、工作树和索引的区别
  • redis核心命令全局命令 + redis 常见的数据结构 + redis单线程模型
  • TCP、HTTP、RPC
  • java 网络编程 详解
  • 数据结构判断两棵树是否相等
  • 九,[极客大挑战 2019]LoveSQL1
  • JavaWeb—— 构建互联网世界的 “魔法砖石” 与实战密码
  • 企业品牌曝光的新策略:短视频矩阵系统
  • 多模态抑郁估计论文研读|Multi-modal Depression Estimation Based on Sub-attentional Fusion
  • 【QNX+Android虚拟化方案】123 - 如何配置qnx侧GPIO_IRQ中断和PMIC_GPIO_IRQ中断
  • 【Android】View工作原理
  • Linux 内核系统架构
  • Kafka-Consumer源码分析
  • USB 声卡全解析:提升音频体验的得力助手
  • 网络安全之常用安全设备功能及作用_设备管理器安全设备是什么
  • Runway 技术浅析(六):文本到视频(Text-to-Video)
  • GPT时代的BI革命:智能报表系统如何颠覆传统决策
  • qt音频实战
  • Vue 实现无线滚动效果
  • Linux下anaconda安装环境
  • Docker和Docker Compose部署方式的区别以及各自适用的场景(ChatGPT-4o回答)
  • WPF+MVVM案例实战与特效(三十一)- 封装一个加载动画的自定义控件
  • 将一个数组逆序输出。-多语言