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

CppCon 2015 学习:Practical Move Semantics

  1. std::unique_ptr 和已删除的拷贝构造函数相关的编译错误
    • 问题背景std::unique_ptr 是一种独占的智能指针,意味着它不允许拷贝构造或拷贝赋值,因为它负责管理的资源只能有一个拥有者。如果你在代码中试图拷贝一个 std::unique_ptr,就会遇到编译错误。通常,这是因为默认的拷贝构造函数已被删除。
    • 解决方案:正确地使用 std::move 来移动 unique_ptr,而不是试图拷贝它。
  2. 你是否曾经不确定是否触发了容器的拷贝或移动操作?
    • 问题背景:在 C++ 中,容器的拷贝和移动操作可能会因不同的语法或函数调用而有所不同。对于大型容器或资源密集型对象,这种区分尤其重要。你可能会遇到不清楚代码是否进行了拷贝还是移动的情况。
    • 解决方案:使用 std::move 显式触发移动语义,确保你在需要的时候进行资源的转移,而不是拷贝。检查编译器的警告和错误信息,有时它们可以帮助你理解是否发生了拷贝。
  3. 你是否曾经怀疑调用 std::move 是否必要?
    • 问题背景std::move 是一种强制性转换,它将对象标记为可移动(而不是可拷贝)。然而,std::move 并不实际“移动”对象,它只是为编译器提供一个提示,允许使用移动语义。你可能会疑惑是否每次调用 std::move 都是必需的,或者是否有可能不小心使用了它。
    • 解决方案:只有在你需要将对象的所有权转移时,才使用 std::move。如果你不需要转移所有权,只是进行拷贝操作,std::move 就不应该被调用。要小心,std::move 之后的对象通常不应再被使用,除非你显式地重新赋值或重置它。

总结

这些问题都涉及 C++ 中的移动语义和拷贝构造函数的使用。C++ 的智能指针和容器通过提供明确的拷贝和移动控制来帮助管理资源。了解何时使用拷贝,何时使用移动,是编写高效、无内存泄漏代码的关键。

  • std::unique_ptr:它的拷贝构造函数被删除,使用时要确保通过 std::move 进行移动,而不是拷贝。
  • 拷贝和移动操作:检查是否显式使用 std::move,避免不必要的拷贝,特别是对于大型对象。
  • std::move 的使用:只有在需要将对象的所有权转移时使用 std::move,并确保在移动后,源对象不再使用。

代码分析:std::unique_ptr 和所有相关示例

1. NewFoo() 函数

std::unique_ptr<Foo> NewFoo() {return std::make_unique<Foo>(1);
}
  • 目的:创建并返回一个 std::unique_ptr<Foo>,它管理一个 Foo 对象。
  • std::make_unique<Foo>(1) 创建一个 Foo 对象,并返回一个 unique_ptr。这确保了该对象的生命周期由 std::unique_ptr 管理,不会被意外删除。
    2. AcceptFoo() 函数
void AcceptFoo(std::unique_ptr<Foo> f) { f->PrintDebugString(); 
}
  • 目的:接受一个 std::unique_ptr<Foo>,并通过该指针调用 PrintDebugString() 方法。
  • std::unique_ptr<Foo> 作为值传递给 AcceptFoo,意味着传入的 unique_ptr 被转移到 f,因此调用函数后,原始的 unique_ptr 不再有效。

3. Simple() 函数

void Simple() {AcceptFoo(NewFoo()); // 函数调用
}
  • 目的:直接传递 NewFoo() 的返回值(即 std::unique_ptr<Foo>)到 AcceptFoo
  • 由于 std::unique_ptr 不能拷贝,只有通过移动(std::move)才能传递给 AcceptFoo,此代码可以编译并正常工作,因为 std::move 是隐式完成的。

4. DoesNotBuild() 函数

void DoesNotBuild() {std::unique_ptr<Foo> g = NewFoo();AcceptFoo(g); // DOES NOT COMPILE!
}
  • 问题:这段代码不编译。
  • 原因std::unique_ptr 不允许拷贝,只能移动,因此尝试将 g 传递给 AcceptFoo 会导致编译错误。
    • 如果你想将 g 传递给 AcceptFoo,必须使用 std::move(g),否则编译器将无法通过类型匹配。

5. SmarterThanTheCompilerButNot() 函数

void SmarterThanTheCompilerButNot () {Foo* j = new Foo(2);std::unique_ptr<Foo> k(j);std::unique_ptr<Foo> l(j); // DOUBLE DELETE ERROR
}
  • 问题:这里创建了两个 std::unique_ptr,它们都管理同一个裸指针 j。这会导致 双重删除(double-delete) 错误。
  • 原因std::unique_ptr 会自动删除它管理的资源。如果两个 unique_ptr 同时管理相同的资源,它们都会在销毁时尝试删除相同的内存,导致运行时崩溃。
    • 这违反了 std::unique_ptr 的唯一性原则。每个 unique_ptr 应该独占一个资源,不能共享同一个资源。

6. EraseTheName() 函数

void EraseTheName() {std::unique_ptr<Foo> g = NewFoo();AcceptFoo(std::move(g)); // DOES COMPILE!
}
  • 目的:这段代码展示了如何通过 std::move(g)std::unique_ptr g 转移到 AcceptFoo()
  • 解释
    • std::move(g) 显式地将 g 的所有权转移到 AcceptFoo。因此,g 在传递给 AcceptFoo 后将变得不可用。
    • 这段代码 编译成功,并符合 std::unique_ptr 的移动语义。

7. std::vector<Foo> 示例

std::vector<Foo> NewFoo() {return std::vector<Foo>({1});
}
  • 目的:返回一个 std::vector<Foo> 对象。
  • 这段代码展示了如何通过 std::make_uniquestd::move 来返回一个 std::vector,这是 C++ 中管理动态数组的一种常见方式。

8. AcceptFoo() 用于 std::vector<Foo>

void AcceptFoo(std::vector<Foo> f) {}
  • 目的:接受一个 std::vector<Foo> 对象。
  • 解释:与 std::unique_ptr 类似,std::vector 也有移动语义。你可以将一个 std::vector<Foo> 通过移动传递,而不会发生拷贝。

9. EraseTheName() 中的 std::move(g)

void EraseTheName() {std::vector<Foo> g = NewFoo();AcceptFoo(std::move(g)); // DOES NOT COPY!
}
  • 目的:通过 std::move(g)std::vector<Foo> 转移到 AcceptFoo,而不是进行拷贝。
  • 解释
    • 这段代码 不进行拷贝,而是将 g 移动到 AcceptFoo,从而避免不必要的拷贝操作。
    • 使用 std::move 使得 g 的资源被转移,而不是复制,确保了性能上的优势。

总结:

  1. std::unique_ptr:只能传递(或移动)它的所有权,不能拷贝。每次你想将 unique_ptr 传递给函数时,都需要确保使用 std::move 来明确地将所有权转移。
  2. std::move:用于显式地将一个对象的所有权从一个 unique_ptr 移动到另一个地方。如果不使用 std::move,就会导致编译错误。
  3. 避免资源双重删除:通过 std::unique_ptr 的唯一性,确保没有多个 unique_ptr 管理同一资源,否则会导致运行时的 double-delete 错误。
  4. 智能指针的使用规则:智能指针的设计原则是避免资源管理上的错误,并避免不必要的资源拷贝,确保对象生命周期的正确性。
    通过这些示例,可以更好地理解如何使用 std::unique_ptrstd::move 来正确管理资源和避免常见的错误。

代码分析

1. 两个名字:一个复制

vector<int> foo;
FillAVectorOfIntsByOutputParameterSoNobodyThinksAboutCopies(&foo);
vector<int> bar = foo; // Yep, this is a copy.
map<int, string> my_map;
string forty_two = "42";
my_map[5] = forty_two; // Also a copy: my_map[5] counts as a name.
  • 分析
    • foo 被复制给 bar:这行代码 vector<int> bar = foo; 是一个拷贝构造,它会创建一个新的 vector<int>,并将 foo 中的所有元素拷贝到 bar 中。
    • my_map[5] = forty_two;:在 my_map[5] 这行代码中,forty_two 会被复制到 my_map 的元素中。map 是通过拷贝赋值将 forty_two 字符串存储在 my_map[5] 中。
    • 总结
      • 拷贝发生时,每当我们有两个名字指向同一个数据(例如 foobar)时,数据就会被复制。这通常发生在通过拷贝构造函数或者赋值操作符时。
2. 一个名字:一个移动
vector<int> GetSomeInts() {vector<int> ret = {1, 2, 3, 4};return ret;
}
// Just a move: either "ret" or "foo" has the data, but never both at once.
vector<int> foo = GetSomeInts();
// Also a move: std::move makes the old name no longer count.
vector<int> bar = std::move(foo);
  • 分析
    • GetSomeInts 函数:该函数创建了一个 vector<int>,并返回它。返回时,ret 的内容不会被拷贝,而是通过移动语义将其传递给调用者(例如 foo)。移动操作比拷贝更高效,因为它不会复制元素,而是直接转移资源的所有权。
    • std::move(foo):通过 std::movefoo 的数据被移动到 bar 中,而不是复制。这是通过将 foo 的资源“转交”给 bar,并使 foo 变为一个有效但空的对象来完成的。foo 不再拥有原来的数据,而 bar 拥有该数据。
    • 总结
      • 移动发生时,通过 std::move 或返回局部变量时的返回值优化(如返回局部变量 ret),数据会被“移动”而不是“复制”。这意味着所有权转移,不会再有多份数据副本。
3. 没有名字:临时对象
void OperatesOnVector(const vector<int>& v);
// No copies: the values in the vector returned by GetSomeInts()
// will be moved (O(1)) into the temporary constructed between these
// calls and passed by reference into OperatesOnVector().
OperatesOnVector(GetSomeInts());
  • 分析
    • 临时对象GetSomeInts() 返回一个临时 vector<int>,然后该临时对象被传递给 OperatesOnVector。由于 OperatesOnVector 接受的是 const vector<int>&(即传引用),它会避免对临时对象进行复制。
    • 临时对象的生命周期会随着函数调用结束而结束,但在调用过程中,它的资源可能通过移动语义被传递(而不是拷贝)。
    • 总结
      • 没有名字的临时对象,如 GetSomeInts() 返回的临时 vector<int>,当它通过引用传递给函数时,不会进行拷贝操作。如果函数参数接受的是 const T&,且传递的是临时对象,则可以利用移动语义(如果可能的话)进行更高效的操作。

关键概念总结

  1. 两个名字:拷贝
    • 当有两个对象(如 foobar)指向同一数据时,数据会被拷贝。此时会调用拷贝构造函数或赋值操作符。
    • 例如,vector<int> bar = foo; 会将 foo 中的数据复制到 bar
  2. 一个名字:移动
    • 移动是将数据的所有权从一个对象转移到另一个对象,而不是复制数据。通过 std::move 或函数返回值优化可以实现。
    • 例如,vector<int> bar = std::move(foo);foo 的数据移动到 bar 中,foo 不再拥有数据。
  3. 没有名字:临时对象
    • 临时对象(如 GetSomeInts() 返回的 vector<int>)在传递时,如果是通过引用传递,并且没有进行复制操作,数据可能通过移动语义传递给函数。
    • 例如,OperatesOnVector(GetSomeInts()); 中,GetSomeInts() 返回的临时对象会直接传递给 OperatesOnVector,并且不会进行不必要的复制。

小结

  • 拷贝 发生在有两个名字时,数据被复制到新对象。
  • 移动 发生在只有一个名字时,数据的所有权从一个对象转移到另一个对象。
  • 临时对象 通过引用传递给函数时,通常会避免拷贝,通过移动语义(如果可能)更高效地进行操作。

代码分析

1. foo() 返回 std::vector<string>:O(1) 还是 O(n)?
std::vector<string> foo() {std::vector<string> ret;ret.resize(100);for (int i = 0; i < 100; ++i) {ret.push_back(std::to_string(i));}return ret;
}
void f() {auto vec = foo();  // O(1) or O(n)?
}
  • 分析
    • 在函数 foo() 中:
      1. 创建了一个 std::vector<string> ret
      2. 使用 resize(100)ret 的大小调整为 100。
      3. 然后用 push_backret 中添加 100 个字符串(通过 std::to_string(i) 转换数字为字符串)。这实际上是调用了 100 次 push_back,每次插入一个新元素。
    • return ret
      • 返回一个 std::vector<string>,由于 C++11 引入了返回值优化 (RVO),编译器通常会进行优化,避免不必要的拷贝操作。通常情况下,这样的返回值会使用移动语义而不是拷贝。
      • 这意味着 ret 并不是被拷贝到 vec 中,而是直接“移动”到 vec,这通常是 O(1) 时间复杂度。
      • 但是在某些情况下,如果编译器不能进行优化,可能会发生 O(n) 的拷贝(但这非常少见,特别是在现代编译器中)。
    • 总结
      • O(1)O(n),具体取决于编译器是否能够进行移动优化(如果能够的话,则是 O(1))。
      • 在没有特殊优化的情况下,返回的 std::vector 会涉及到拷贝操作,因此是 O(n) 的时间复杂度。
2. 使用静态变量 ret 的情况
std::vector<string> foo() {static std::vector<string> ret;ret.resize(100);for (int i = 0; i < 100; ++i) {ret.push_back(std::to_string(i));}return ret;
}
void f() {auto vec = foo();  // O(1) or O(n)?
}
  • 分析
    • 这次,ret 被声明为 static。这意味着它在函数调用之间会保持其状态(即:ret 是持久化的,跨多个函数调用)。
    • ret.resize(100) 会调整 ret 的大小为 100。
    • 然后使用 push_backret 添加 100 个元素。注意static 变量只会在第一次调用时初始化一次,此后的调用会重复使用同一个 ret,不需要重新初始化或分配内存。
    • return ret
      • 由于 ret 是静态的,编译器会返回对这个静态变量的引用(而不是复制它),通常这会导致 O(1) 的操作。
      • 不过,即使 ret 是静态的,如果 ret 存储了大量数据(比如此处的 100 个字符串),返回它时会涉及到对数据的移动或者拷贝操作。
    • 总结
      • O(1):因为 ret 是静态的,返回时只涉及到对现有数据的移动(或者引用传递),而不需要重新分配和初始化。
      • 不会有 O(n) 的开销,因为数据的分配是一次性的,且返回时不会重复分配内存。
3. 使用 std::move 进行移动语义
std::vector<string> foo() {std::vector<string> ret;ret.resize(100);for (int i = 0; i < 100; ++i) {ret.push_back(std::to_string(i));}return ret;
}
void f() {auto vec = std::move(foo());  // O(1) or O(n)?
}
  • 分析
    • 在这段代码中,foo() 返回一个 std::vector<string>,然后通过 std::move 将返回值从 foo() 移动到 vec 中。
    • std::move 实际上并不做移动操作,它只是强制将对象转换为一个右值引用,使得编译器可以调用移动构造函数而不是拷贝构造函数。
    • 因此,在这个例子中,std::move(foo()) 触发了对 std::vector<string> 的移动构造。
    • 移动构造函数通常会将资源的所有权从源对象转移到目标对象,而不会进行数据的深度复制,因此它是 O(1) 时间复杂度。
    • 总结
      • O(1):由于 std::move 的存在,数据的所有权从 foo() 中的临时对象转移到了 vec 中,不涉及数据复制,因此是常数时间复杂度。
4. 两个额外的奇怪笔记
  • 不触摸已移动的对象
    • 经过 std::move 的对象(如 std::move(foo()))会变成“已移动的对象”。这种对象处于不确定的状态,它们不再包含有效的数据。使用这些对象会导致未定义的行为。
    • 通常建议在移动之后,不再使用这些对象,避免访问其“已移动的”内容。
  • std::move 本身不做任何事情
    • std::move 只是一个类型转换,它将对象转换成右值引用。实际上,没有任何数据移动的操作,所有的工作都由对象的移动构造函数或移动赋值操作符完成。
    • std::move 不会“移动”数据,它只是“标记”对象为右值,让编译器知道可以使用移动构造函数。

总结

  1. O(1) 或 O(n):返回 std::vector<string> 的时间复杂度取决于编译器是否能够进行优化(如移动语义),如果能优化就是 O(1),否则就是 O(n)。
  2. 静态变量:使用静态变量时,通常返回的是对该静态变量的引用,因此是 O(1) 操作。
  3. std::move:通过 std::move 触发移动语义,通常是 O(1) 操作,因为只涉及资源的所有权转移,而没有复制数据。
http://www.xdnf.cn/news/13327.html

相关文章:

  • SpringBoot+Vue+MySQL全栈开发实战:前后端接口对接与数据存储详解
  • 【算法篇】逐步理解动态规划模型5(子序列问题)
  • 隐藏wordpress后台登陆地址 让wordpress网站更安全
  • 【VBA】使用脚本把doc/docx转换为pdf格式
  • 消息消费类型和具体实现
  • nsswitch.conf配置文件内容解析
  • 生产安全与设备管理如何分清界限?如何正确用设备管理系统?
  • 微机原理与接口技术,期末冲刺复习资料(五)
  • 3.1 数据链路层的功能
  • 商品中心—2.商品生命周期和状态的技术文档
  • HTML 、CSS 、JavaScript基本简单介绍
  • 大型活动交通拥堵治理的视觉算法应用
  • ceph集群调整pg数量实战(下)
  • 【如何用Python调用DeepSeek的API接口?】
  • JavaSec-RCE
  • Python爬虫实战:爬取知乎回答详情
  • WebRTC(二):工作机制
  • CARSIM-车速、油门、刹车练习
  • 【计网】作业7
  • 金属矫平机:塑造平整与精度的工业利器
  • 【机器视觉】单目测距——运动结构恢复
  • synchronized 学习
  • 计算机网络笔记(三十四)——5.6TCP可靠传输的实现
  • 【持续更新】linux网络编程试题
  • 优化篇 | 网络时延优化有哪些项
  • ARM 单片机定义变量绝对地址方法
  • umask命令详解
  • 如何在Debian中提高phpstorm的稳定性
  • PostgreSQL 安装与配置全指南(适用于 Windows、macOS 与主流 Linux 发行版)
  • <6>-MySQL表的增删查改