在探讨了RAII和智能指针之后,我们看到C++如何通过所有权模型来管理堆上资源的生命周期。unique_ptr的“独占所有权”和shared_ptr的“共享所有权”,将资源管理的责任从模糊的约定提升为编译器可检查的类型系统特性。

然而,所有权思想的价值远不止于智能指针。当我们审视值语义对象时,一个自然的优化思路浮现了:如果某些对象只是临时存在的“中间产物”,我们能否不进行昂贵的深拷贝,而是“移动”其内部资源的所有权?

这就是C++11引入移动语义的核心动机。移动语义实际上是所有权思想在值语义领域的自然延伸:

  • unique_ptr的移动是“堆资源所有权的转移”

  • 普通对象的移动是“对象内部资源所有权的转移”

通过移动语义,C++实现了一个精妙的平衡:既保持了值语义的简洁性和确定性,又在需要时避免了不必要的拷贝开销。配合RVO/NRVO等编译器优化,现代C++让值语义对象既安全又高效。

本文将探讨移动语义的完整技术栈:从左值/右值的本质区分,到右值引用如何支持移动语义,再到完美转发如何解决通用代码中的值类别保持问题。


左值、纯右值与将亡值

这些值类别是表达式属性,而不是对象的属性,因为对于同一个变量,其表达式不同,会有不同的值类别,比如x、std::move(x)

  • 变量(variable)是有名字的对象
  • 对象是(object)存储某个类型的值的内存区域(memory)
  • (value)是按照类型进行解释的比特集合
  • 类型(type)定义一组可能的值以及一组(作用对象的)操作

左值表达式是一个具有身份的表达式,表示一个可以持续存在的对象,可以取地址。从资源所有权的角度来看,其不会触发移动语义,其资源所有权依然被该对象持有且该对象长期存在(相对于右值),需要先通过std::move将左值转换为一个将亡值,并触发移动语义,才能将资源的所有权转移。(特殊的,字符串常量虽然是左值,但是可以被移动)

纯右值表达式是一个不具有身份表达式,并不对应一个具体的对象专门用于计算或初始化对象。比如字面常量,直接编码在指令中,再比如函数的返回值,是临时存储在寄存器中的值,无法获取到其地址。从资源所有权的角度来看,其表示一个”尚未被任何具体对象拥有的资源”,其所有权会在初始化/赋值时直接转移给目标对象

将亡值表达式具有身份,比如,std::move(s)就是一个将亡值,其对应的对象依然是原来的对象s。从资源所有权的角度来看,相比于左值,其即将消亡,正准备放弃资源所有权,所以其可以触发移动语义,将资源所有权转移给其他对象

较为特殊的是,在C++下字符串常量和普通常量不同,字符串常量的类型是const char[]字符数组,其存储于只读数据段.rodata,可以被取地址,是一个左值,而不像其他常量是右值


将亡值和纯右值的区别在哪呢?

二者最本质的区别在于,前者是对象,后者是值。将亡值相较于纯右值来说是有身份的,纯右值表示一个纯粹的值,不对应任何对象,用于初始化对象,其内嵌在指令中、存在于寄存器中等等;而将亡值表示一个已经存在的对象,只是该对象的资源可以被转移,通常在栈、堆上


右值引用与移动语义

为什么需要右值引用?

返回值优化:函数的返回值是一个右值rvalue,如果通过变量接收该右值,相比于const 引用接收,会导致额外的一次拷贝构造+析构,即将匿名空间中的值拷贝到接收变量中,再将匿名空间析构。而const引用接收的原理是,让引用直接绑定这块匿名空间,并不再释放立即释放这块匿名空间,而是随着引用成为了栈上的变量。 但是,其虽然省去了调用,但是常引用是const修饰的,不可修改。于是,引入了右值引用。其在行为上和const引用几乎是一模一样的,区别只在于由于没有const修饰,所以可改。 但是,这并不是右值引用的主要功能,因为在编译器的RVO/NRVO优化下,就算直接用变量接收函数返回值,同样不会触发额外的拷贝构造+析构,会在接收变量的内存区域上直接构造

struct Test {
  Test() {}
  ~Test() {}
};

Test Demo1() {
  Test t;
  return t;
}

void Demo2() {
  Test &&t = Demo1();
}

支持移动语义:

RVO/NRVO主要针对的是返回值做优化,而对于传参、赋值等移动语义领域,需要移动语义减少对象拷贝的代价,这里才是右值引用的主要发挥场景

移动语义优化——由于右值对象一般是临时对象,所以如果新对象想要获取右值对象的资源,应该直接让新对象获取资源的所有权,而不是拷贝+删除,以降低传参拷贝时的代价,这也是为什么需要移动语义的原因。

为了能让函数接收右值,优先支持移动语义(而不是调用常引用参数版本),所以引入了右值引用。相比于左值引用,左值引用代表了该资源对象还需要被使用,而右值引用代表着我们可以对该对象的资源进行转移。

比如移动构造函数​ 和 移动赋值运算符​:在移动赋值函数中,不会再对资源进行“深拷贝”,而是通过调整指针指向,达到“转移资源所有权”的目的。

String::String(String &&str): buf_(str.buf_) { // 直接浅复制
  str.buf_ = nullptr;
}

在通过右值引用接收到右值后,如何转发、处理该右值?

该问题的本质是,参数是作为一个具名的左值(具体为什么参数是左值,见下文解释),那么如何将表达式的值类型从左值转换为右值,从而传递给参数为右值引用的函数,以将资源移动? 为了解决该问题,所以引入了std::move()

【如何理解std::move()?】std::move()本质上是static_cast< T&& >,将变量表达式值类型从左值变为一个右值,把对象标记为“资源可以被移动”,将资源的所有权转移,其和右值引用参数相匹配,进而触发移动语义,真正的让资源被转移。

这里有两个关键点:

一是std::move()不会改变对象本身的性质,其依旧在栈/堆上存储着,只是在表达式层面将它“从左值转换为右值”;

std::string s = "hello";

std::move(s);  // 表达式是右值
s;             // 表达式仍然是左值

二是std::move()本身并不会进行任何移动操作,其只是一个强制类型转换的模板函数,允许触发移动语义,让资源被移动,而真正的资源移动操作需要是在函数中进行的,比如移动构造/移动赋值函数。 三者的关系为:

  • std::move:发出“可以移动”的信号,将资源所有权转移
  • T&&:接收这个信号
  • 移动构造函数(或其他函数中):实际执行资源转移

总结一下,有了移动语义后传参的行为:该行为取决于形参类型和实参,如果形参是按值传递的,则一定会触发构造,区别在于是拷贝还是移动——如果实参是左值,则为拷贝; 如果实参是右值,则为移动。 而如果形参是左值引用或右值引用传递的,则不会触发构造。

void func(std::string s);
std::string str = "hello";

func(str);              // 左值 → 拷贝构造
func(std::move(str));   // 右值 → 移动构造
func("hello"); 

关于func(“hello”):“hello”是一个const char*类型的左值,如果考虑C++17的“强制拷贝消除”优化,prvalue不再先构造临时对象,而是直接初始化目标对象。

如果不考虑优化,则构造一个std::string临时对象,再调用拷贝构造,多产生了一次构造、一次移动、一次析构。


std::move底层原理:

本质上是一个强制类型转换,将对象的值类别转换为右值

template<typename T>
typename std::remove_reference<T>::type&& move(T&& t) noexcept {
    using ReturnType = typename std::remove_reference<T>::type&&;
    return static_cast<ReturnType>(t);
}
  • 参数为万能引用,保证std::move()可以接收任意值类别

  • remove_reference:去掉模板推导实例化结果的引用类型。比如,如果传入一个左值,则模板推导为int&,如果直接返回T&&,则依然返回的是一个左值。所以,需要去除引用类型

  • static_cast< T&&>(t):将表达式的值类别从左值转换为右值。需要注意的是,t是一个左值表达式


为什么“右值引用变量”是左值?

类型和值类别是两个概念。右值引用描述的是一个“类型”,就像int类型、double类型一样,而值类别描述的是表达式,比如Test&& t = Demo(),t作为一个表达式(单个变量名也是表达式)是一个左值,其可以有具体的身份、可以取地址、可以被赋值,只不过其绑定的是一个右值。

比如,下面这个例子,虽然有万能引用,但是表达式t始终是一个左值,调用的结果是lvalue:123 lvalue:123

void TestValue(int& value)
{
    std::cout << "lvalue: " << value << '\n';
}

void TestValue(int&& value)
{
    std::cout << "rvalue: " << value << '\n';
}

template<typename T>
void Print(T&& t)
{
    TestValue(t);
}

int main()
{
    Print(123);
    int num = 456;
    Print(num);
}

从使用的角度来说为什么右值引用变量不能是右值:如果右值引用变量是一个右值,会导致对象被“隐式地”多次移动。所以,必须通过std::move()来“显示地”触发移动语义的的操作,由用户手动控制,而不能是隐式发生的

假设右值引用变量是一个右值:
void process(std::string&& s) {
    save(s);
    log(s); //错误 因为s的资源已经被转移走了
}

什么应用场景下使用std::move()?【Effective Modern C++ 条款25】

让std::move和右值引用配合使用。如果当前函数的参数值类别是右值引用类型的,则代表实参不再需要该对象资源了,所以需要通过move将形参继续转换为右值传递下去,触发移动语义来转移其资源。

移动构造/移动赋值的实现:

class A {
public:
    std::string s;

    A(A&& other)
        : s(std::move(other.s)) {}   // 必须 move

    A& operator=(A&& other) { //other绑定的对象是一个右值 
        s = std::move(other.s);      // 必须 move
        return *this;
    }
};

又或者,我们主动通过std::move()将表达式值类型,从左值转换为右值,传递给参数为右值引用的函数,以将资源移动。 比如容器插入的场景:

std::vector<std::string> v;
std::string s = "hello";

v.push_back(s);              // 拷贝
v.push_back(std::move(s));   // 移动(推荐)
v.push_back("hello");        //注意,这里不会触发移动,因为“hello”是一个左值,会先通过“hello”构造一个字符串的临时对象,再绑定到调用拷贝构造,绑定到const std::string&
v.push_back(std::string("hello"));  // 创建临时 string,会调用移动构造函数(如果可行)

std::move使用注意事项:

  • std::move不能完全保证资源被移动,是否真正可以发生移动还却决于”类型“是否允许被修改(即是否为const)。 如果想要一个资源对象被转移,那么就不要将其声明定义为常量

    class A
    {
    public:
        A(const std::string text)
            :value(std::move(text)) //实则依然调用的是拷贝构造 而不是移动构造
        {}
    
    private:
        std::string value;
    }
    

    这是因为std::move(text)的表达式值类别为const std::string&&,而不是std::string&&,无法和匹配上移动构造函数,因为移动本质上是“窃取”资源,而const修饰又不允许修改该对象,是相互矛盾的。所以,编译器会退而求其次地尝试拷贝构造,不会调用移动构造

    std::string(std::string&& other); //移动构造
    std::string(const std::string&& other); //拷贝构造
    
  • 不要在返回前move返回值。比如:

    std::string base_url = tag->GetBaseUrl();
    if (!base_url.empty()) {
      UpdateQueryUrl(std::move(base_url) + "&q=" + word_);
    }
    LOG(INFO) << base_url;  // |base_url| may be moved-from
    

    导致外部接收到该变量后,不能继续使用,否则可能导致未定义行为。此外,最好不要对返回值move,比如:

    std::unique_ptr<int> foo() {
      auto ret = std::make_unique<int>(1);
      //...
      return std::move(ret);  // -> return ret;
    }
    

    C++会自动将离开作用域的非引用类型的返回值当作右值,并通过RVO/NRVO进行优化,而std::move()会阻止编译器优化

RVO/NRVO优化实现零拷贝:

何时触发?
前者在返回匿名临时对象时触发,比如return {1,2},C++17起,其被规定为强制优化行为;后者在返回具名的局部对象,且返回类型和函数签名种的返回类型一致时触发,比如 A a; return a;

但是,在几种情况下可能无法触发NRVO,仍需移动语义兜底处理:返回表达式,比如a+b,std::move(p)等;返回函数参数(因为函数参数是调用方已经创建好的对象,没法在函数接受变量的内存空间上直接构造);多个返回路径返回不同对象(没法在编译期判断究竟会构造哪个对象);条件运算符返回不同对象;异常路径等等

为什么RVO可以被强制,而NRVO不可以?
RVO之所以可以强制,是因为在这些场景下返回的都是prvalue,C++17标准将其重新定义为“允许在返回值位置直接构造”,少了拷贝/移动构造调用,不会改变“可观察行为”,其是一个匿名对象,没有身份和地址。而NRVO则不一样,其优化的是“真实存在的对象”,其有名字、有地址、有生命周期,这种优化实际上会改变“可观察行为”,比如地址的值、构造/拷贝/析构次数问题等,所以只能作为编译器优化,而不能作为语言强制规则

如果NRVO失败了,会怎么做?
NRVO失败时,如果是局部变量,会退而求其次地使用移动语义(两次move),如果没有move构造再使用拷贝拷贝

RVO/NRVO优化是怎么做到的?
假设调用函数为B,被调用函数为A。编译器会为函数A添加一个传递参数,比如原先的函数原型是Object create(),实际上转换为void create(Object* __heidden_result),调用方从result = create()变为create(&result)

B在调用A前,会在自己的函数栈帧中提前开辟内存区域,并将这块内存的起始地址作为参数传入到A中。A直接在这块预留的内存位置上构造obj,直接省去了两次拷贝和析构。比如T result = a + b,则直接将a+b的结果放到result中。

效果即为最右侧一图:


移动构造与移动赋值函数

编译器什么时候会生成默认的移动构造/移动赋值函数?

如果用户没有自定义拷贝构造/拷贝赋值/析构函数,则编译器才会生成默认的移动构造/移动赋值函数。实际上,我们不需要记住这一点,只需要遵循Rule of Five即可——一旦要手动管理资源,手动定义了拷贝构造/拷贝赋值/析构函数/移动构造/移动赋值函数中的一个,那么应该也手动定义其余的成员函数

为什么编译器只要在”无自定义资源“的情况下,才会生成默认的移动构造/移动赋值?

【可以和”为什么编译器不会生成默认的拷贝构造/赋值重载“联系起来编译器生成的默认的拷贝构造/赋值重载都是按对象一个个浅拷贝的】编译器生成的默认的移动构造/移动赋值都是逐成员move的。但不会自动帮我们把源资源置空(other.ptr = nullptr),存在双重释放的风险。 这是因为编译器根本理解不了我们的资源语义,不知道我们对于move后的对象是否由额外的状态约束,不知道是否允许空状态,所以不会帮我们把源资源置空

具体来说,对于自定义类型成员,会看是否实现了移动构造,如果实现了就调用移动构造,没实现就调拷贝构造,如果自定义类不能拷贝和移动,则会报编译错误,比如包含const成员和引用成员;

对于内置类型成员直接会按字节拷贝,因为它们没有”资源“,只有”值“

T(T&& other)
    : m1(std::move(other.m1)),
      m2(std::move(other.m2)),
      ...
{}

只有在成员都是标准库类型,或者没有任何要手动管理的资源时,才能采用编译器默认生成:std::string std::vector都已经正确地实现了move,不会出错

class A {
public:
    A(A&&) = default;
    A& operator=(A&&) = default;

private:
    std::string s;
    std::vector<int> v;
};

如何写一个正确的移动构造?

转移资源所有权+将源对象置为”安全状态“+无异常

class String {
public:
    String(String&& other) noexcept
        : buf_(other.buf_),nums_(other.nums_) // 1️⃣ 偷资源
    {
        other.buf_ = nullptr; // 2️⃣ 防止 double free
    }

private:
    char* buf_;
    vector<int> nums_;
};

如何写一个正确的移动赋值运算符?

String& opeartor(String&& other) noexcept
{
    if(this != other)  // 1️⃣ 防自移动
    {
        delete[] buf_;  //2️⃣ 释放旧资源
        this.buf_ = other.buf_;  // 3️⃣ 接管资源
        this.nums_ = std::move(other.nums_);
        other.buf_ = nullptr;  // 4️⃣ 清空源对象 vecotr类型有相应的移动赋值来清理
    }
    return *this;
}

为什么要是noexcept的?

移动操作需要标价为noexcept,是因为标准库在需要提供”强异常安全保证“时(比如vector扩容),只有在确认移动操作不会抛异常的前提下,才会使用移动语义,否则会退化为使用拷贝。这是为了保证,在异常发生时能够正确地回滚状态,如果move到一半出现了异常,源对象的资源已经被”窃取“,无法回滚。相比之下,拷贝完全可回滚,源对象的资源完好无损

那我们应该怎么保证noexcept呢?noexcept标记并不保证函数内部不抛异常,只是保证异常不会逃出当前函数;一旦异常逃出,程序会直接调用std::terminate终止执行。如果只是单纯的指针交换进行资源转移,不调用可能抛异常的函数,比如分配内存,则可以写noexept。具体异常相关内容,后续会出一篇文章再详细说说


引用折叠与万能引用

为什么需要有引用折叠? C++不允许直接定义引用的引用,比如int& && r= i,但是在模板推导的情况下,完全可能出现引用的引用,比如:

template<typename T>
void func(T&& parme)

所以,C++制定了一个统一的规则——引用折叠,使得类型系统能够正常工作。规则——只要有一个引用是左值引用,那么结果就是左值引用。

&  +  &  →  &
&  + &&  →  &
&& +  &  →  &
&& + && →  &&

为什么是左值优先?

而之所以是“左值优先”(左值和左值引用),是因为左值代表了“有身份,需要被持续使用的对象”,所以为了防止原对象的资源错误地被移动,推导链条中只要一个表达式是左值,那么就不能将其推导为右值,而是保守的处理为左值。尽管有可能右值被误判为左值,但其代价只是性能上的损耗,而误判为右值的代价就是语义完全错误,可能出现意想不到的错误

那为什么这样规定呢?

这是为了和模板推导规则相配合,从而实现万能引用。而万能引用的作用在于,让一个函数就能够同时接收左值和右值,与完美转发像匹配,为正确处理、转发左值和右值的打下基础。 但是要注意,万能引用没有保留“值类别信息”,因为其是一个具名变量,始终是左值,需要通过完美转发恢复“值类别信息”!(模板推导规则——对于左值,推导为“引用类型”,比如int&;对于右值,推导为“非引用类型”,比如int。后续会再出一篇文章详细说说模板相关内容)

template<typename T>
void func(T&& parme)

int num = 10;
func(num); //T会被推到为int& 最终模板函数参数实例化为为int&
func(10)   //T会被推到为int 最终模板函数参数实例化为为int&&

什么场景下才可以触发万能引用?【Effective Modenr C++ 24条款】

万能引用 = 模板推导 + 引用折叠,如果其中之一不符合,则无法触发

  • 模板参数T必须在调用点处参与类型推导,其不能是类模板已经确定的参数或调用处显示指定。 因为万能引用依赖于模板推导实例T来和引用折叠配合,从而正确地接受参数。比如,下面的例子就不是万能引用,因为T在类实例化时就已经确定了,其是一个普通的右值引用:

    template<typename T>
    struct A {
        void f(T&& x);  // ❌ 不是万能引用
    };
    
  • 不能对T施加额外的修饰和变换。比如const修饰、容器修饰

    const T&&        // ❌ 有 const 修饰
    T&               // ❌ 左值引用
    std::vector<T>&& // ❌ T 被包裹
    T*&&             // ❌ 指针类型
    

另外,auto&&也是万能引用,因为auto的推导规则,本质上和模板推导类似

int a = 10;
auto&& x = a;   // x 是 int&
auto&& y = 10;  // y 是 int&&

完美转发

为什么需要完美转发?

template<typename T>
void print(T & t){
    std::cout << "Lvalue ref" << std::endl;
}

template<typename T>
void print(T && t){
    std::cout << "Rvalue ref" << std::endl;
}

template<typename T>
void testForward(T && v){ 
    print(v); //v是个左值表达式了,永远调用左值版本的print
    print(std::forward<T>(v)); //本文的重点
    print(std::move(v)); //std::move(v)是个右值表达式,永远调用右值版本的print

    std::cout << "======================" << std::endl;
}

int main(int argc, char * argv[])
{
    int x = 1;
    testForward(x); //实参为左值
    testForward(std::move(x)); //实参为右值
}

运行结果:

Lvalue ref
Lvalue ref //完美转发
Rvalue ref
======================
Lvalue ref
Rvalue ref //完美转发
Rvalue ref
======================

【重点区分”值类别“和”数据类型“】万能引用只负责”能同时正确地接收左值和右值“,其保留了”类型信息“,但丢失了”值类别信息“。如果函数需要对这些参数进行”转发“——即该函数本身不”消费“(存储/拷贝),只是负责将这些参数“转交”给目标函数进行处理,则会导致无法调用到以右值引用为形参的函数。这是因为,在多层函数调用的过程中,由于函数参数一定是具名的,其表达式始终是左值,这就会导致值类别信息在使用时丢失,被一视同仁地处理为一个左值(比如上面的testForward函数)

完美转发通过static_cast< T&& >在编译期基于模板T,将表达式原有的值类别回复,保证参数”在函数内部不丢失值类型并正确传递“。【一定要基于模板】


完美转发的实现原理:

通过模板参数T的信息以及static_cast< T&& >来进行值语义的恢复。 如果T推导为引用类型,比如传入num推导为int&,那么由于引用折叠就会恢复为左值;如果T推导为非引用类型,比如传入10推导为int,那么就会恢复为右值。

需要注意的是,std::forward 本身不会“判断值类别”,它只是根据模板参数 T 来“执行转换”,而 T 是否正确,完全取决于是否发生了调用点的类型推导。这就意味着,完美转发的格式必须是:

template<typename T>
void func(T&& param)

此外,我们传给forward的一定是一个左值表达式,而不是右值表达式。因为forward的设计初衷就是”根据已经退化为左值的参数“,输出”恢复后的表达式“并进行正确转发

// 1️⃣ 正常版本:接左值
template<class T>
T&& forward(std::remove_reference_t<T>& arg){
    return static_cast<T&&>(arg);
}
//remove_referencce是为了移除引用 让参数稳定接收一个左值

// 2️⃣ 禁用版本:接右值
template<class T>
T&& forward(std::remove_reference_t<T>&& arg) = delete;

实际上,标准库是绝对禁止传右值的,比如std::forward< int >(10),其并不符合forward的设计语义,forward是为了恢复原始值的类别,其根本没有原始来源


lambda、bind、function是否支持万能引用/完美转发?

bind只支持值拷贝,所以不可能支持完美转发。

在C++14及之后,模板lambda支持完美转发,相比于bind,bind内部在构造可调用对象时,就会把参数存储起来,此时直接变为了左值,失去了值类别信息。而lambda是在调用时接收参数,可以在调用时进行类型推导,没有丢失值类别信息,从而可以实现完美转发,而auto&&则是一个万能引用

auto f = [](auto&& s) { 
    foo(std::forward<decltype(s)>(s)); 
}; 
f(std::string("hello")); // ✅ rvalue

function不支持完美转发。比如lambda用function存储起来,则又会导致无法完美转发,核心是因为std::function作为一个模板类,std::forward的模板参数在std::function实例化时已经固定,无法根据调用时实参变化来推导,调整转发行为;而完美转发是依赖于模板参数在调用点的推导,使得std::forward 能根据 T 的不同(值或引用)生成不同的转换,从而保留值类别。

具体来说,以下面代码为例子,在 std::function< void(std::string)> 中,模板参数 Args… 在实例化时已经固定为 std::string,因此 std::forward(args) 始终展开为 static_cast<std::string&& >(args),不会随着调用时传入左值或右值而变化。

而在真正的完美转发中,std::forward(args) 中的 T 是在函数调用时根据实参推导得到的:

  • 如果传入左值,则 T = std::string&,forward 为左值
  • 如果传入右值,则 T = std::string,forward 为右值

因此完美转发的关键在于 T 的推导发生在调用点,而不是在模板实例化时固定。这一点对于万能引用也是同样的

std::function<void(std::string)> f = [](std::string&& s) {
    foo(std::move(s));
};
f(std::string("hello")); // ❗行为已经变化


template<typename R, typename... Args>
class MyFunction<R(Args...)>  //对函数类型的模板偏特化
{
    //.....
    R operator()(Args... args)  // 通过callable中的func进行实际调用
    {
        if (ptr) {
            return ptr->call(std::forward<Args>(args)...);
        }
    }
    //.....
}    

总结

通过对移动语义的深入探讨,我们看到了C++如何通过一套精巧的类型系统,在保持值语义本质的同时,大幅优化了对象传递的效率:

  • 右值引用为移动语义提供了语言基础,让“窃取”临时对象资源成为可能

  • 移动构造/赋值函数实现了资源所有权的低成本转移

  • 完美转发解决了多层函数调用中的值类别保持问题

  • RVO/NRVO在编译器层面进一步优化了返回值的创建

这套机制共同构成了现代C++高效值传递的基础设施。然而,移动语义的价值最终要落实到具体的函数设计中——我们如何在实际编程中充分利用这些机制,设计出既高效又易用的接口? 碍于篇幅,这个问题会在下一篇文章中来回答