26

我基本上是想弄清楚,整个“移动语义”概念是全新的,还是只是让现有代码更易于实现?我总是对减少调用复制/构造函数的次数感兴趣,但我通常使用引用(可能还有 const)传递对象,并确保我总是使用初始化列表。考虑到这一点(并查看了整个丑陋的 && 语法),我想知道是否值得采用这些原则或像我已经做的那样简单地编码?这里有什么新东西吗,还是我已经做的只是“更简单”的语法糖?

4

6 回答 6

30

TL;博士

这绝对是新事物,它不仅仅是一种避免复制内存的方法。

长答案:为什么它是新的以及一些可能不明显的含义

移动语义正是顾名思义——即一种显式声明移动对象指令而不是复制的方法。除了明显的效率优势之外,这还为程序员提供了一种符合标准的方法来获得可移动但不可复制的对象。可移动且不可复制的对象通过标准语言语义传达了非常清晰的资源所有权边界。这在过去是可能的,但没有标准/统一(或 STL 兼容)的方式来做到这一点。

这是一件大事,因为拥有标准和统一的语义对程序员和编译器都有好处。程序员不必花时间将错误引入编译器可以可靠生成的移动例程(大多数情况下);编译器现在可以进行适当的优化,因为该标准提供了一种方法来通知编译器您何时何地进行标准移动。

移动语义特别有趣,因为它非常适合 RAII 习语,这是 C++ 最佳实践的长期基石。RAII 不仅仅包含这个例子,但我的观点是移动语义现在是一种标准的方式来简洁地表达(除其他外)可移动但不可复制的对象。

您不必总是显式定义此功能以防止复制。一种称为“复制省略”的编译器功能将从按值传递的函数中消除大量不必要的副本。

关于 RAII 的犯罪不完整速成课程(针对初学者)

我意识到您并没有要求提供代码示例,但这里有一个非常简单的示例,它可能会使未来的读者受益,他们可能不太熟悉该主题或 Move Semantics 与 RAII 实践的相关性。(如果您已经理解这一点,请跳过此答案的其余部分)

// non-copyable class that manages lifecycle of a resource
// note:  non-virtual destructor--probably not an appropriate candidate
//        for serving as a base class for objects handled polymorphically.
class res_t {
  using handle_t = /* whatever */;
  handle_t* handle;  // Pointer to owned resource
public:
  res_t( const res_t& src ) = delete;            // no copy constructor
  res_t& operator=( const res_t& src ) = delete; // no copy-assignment

  res_t( res_t&& src ) = default;                // Move constructor
  res_t& operator=( res_t&& src ) = default;     // Move-assignment

  res_t();                                       // Default constructor
  ~res_t();                                      // Destructor
};

此类的对象将在构造时分配/提供所需的任何资源,然后在销毁时释放/释放它。由于数据成员指向的资源永远不会意外转移到另一个对象,因此资源的合法所有者永远不会受到质疑。除了使您的代码不易被滥用或出错(并且易于与 STL 容器兼容)之外,任何熟悉此标准实践的程序员都会立即认识到您的意图。

于 2013-05-29T21:02:11.143 回答
9

在图灵焦油坑,太阳底下没有新鲜事。移动语义所做的一切,都可以在没有移动语义的情况下完成——它只需要更多的代码,而且更脆弱。

移动语义所做的是采用一种特殊的通用模式,该模式在许多情况下极大地提高了效率和安全性,并将其嵌入到语言中。

它以明显的方式提高了效率。对于许多数据类型而言,无论是通过swap还是move构造,移动都比复制要快得多。您可以创建特殊的界面来指示何时可以移动事物:但老实说,人们并没有这样做。使用移动语义,它变得相对容易。比较移动 astd::vector和复制它的成本——move大约需要复制 3 个指针,而复制需要堆分配,复制容器中的每个元素,并创建 3 个指针。

更重要的是,将reserve移动感知std::vector与仅复制感知进行比较:假设您有一个std::vectorof std::vector。在 C++03 中,如果您事先不知道每个组件的尺寸,那将是性能自杀——在 C++11 中,移动语义使它像丝绸一样平滑,因为它不再重复复制子组件- vectors 每当外部向量调整大小时。

移动语义使每个“pImpl模式”类型都具有极快的性能,同时意味着您可以开始拥有表现得像值的复杂对象,而不必处理和管理指向它们的指针。

除了这些性能提升和开放复杂类作为值之外,移动语义还开放了一系列安全措施,并允许做一些以前不太实用的事情。

std::unique_ptr是 的替代品std::auto_ptr。他们都做大致相同的事情,但std::auto_ptr将副本视为移动。这使得std::auto_ptr在实践中使用起来非常危险。同时,std::unique_ptr 只是工作。它非常好地代表了某些资源的独特所有权,所有权的转移可以轻松顺利地进行。

你知道你foo*在一个接口中的问题,有时它意味着“这个接口正在获取对象的所有权”,有时它意味着“这个接口只是想能够远程修改这个对象”,你必须深入研究进入 API 文档,有时还包括源代码来找出哪个?

std::unique_ptr实际上解决了这个问题——想要接管的接口现在可以接管std::unique_ptr<foo>,所有权的转移在 API 级别和调用接口的代码中都很明显。 std::unique_ptr是一个auto_ptr可以正常工作的,并且删除了不安全的部分,并替换为移动语义。它以近乎完美的效率完成所有这些工作。

std::unique_ptr是资源的可转移 RAII 表示,其值由指针表示。

写完之后make_unique<T>(Args&&...),除非你写的是非常底层的代码,否则最好不要再直接调用new。移动语义基本上已经new过时了。

其他 RAII 表示通常是不可复制的。端口、打印会话、与物理设备的交互——所有这些都是“复制”没有多大意义的资源。它们中的大多数都可以轻松修改以支持移动语义,这为处理这些变量提供了很大的自由度。

移动语义还允许您将返回值放在函数的返回部分。通过引用获取返回值的模式(并记录“这个是唯一的,这个是输入/输出的”,或者没有这样做)可以通过返回您的数据来代替。

所以不是void fill_vec( std::vector<foo>& ),你有std::vector<foo> get_vec()。这甚至适用于多个返回值——std::tuple< std::vector<A>, std::set<B>, bool > get_stuff()可以调用,并且您可以通过std::tie( my_vec, my_set, my_bool ) = get_stuff().

输出参数在语义上可以是仅输出的,开销很小(上面,在最坏的情况下,需要 8 个指针和 2 个bool副本,无论我们在这些容器中有多少数据——而且开销可以低至 0由于移动语义,指针和 0bool副本需要更多工作)。

于 2013-05-30T03:15:19.080 回答
6

这里绝对有新的事情发生。考虑unique_ptr哪些可以移动,但不能复制,因为它唯一地拥有资源的所有权。如果需要,可以通过将其移动到新的所有权来转移该所有权unique_ptr,但复制它是不可能的(因为您将有两个对所拥有对象的引用)。

虽然移动的许多用途可能会对性能产生积极影响,但可移动但不可复制的类型是对语言的更大功能改进。

简而言之,使用新技术表明应该如何使用你的类的含义,或者可以通过移动而不是复制和销毁来缓解(重要的)性能问题。

于 2013-05-29T22:46:48.093 回答
2

如果没有参考 Thomas Becker 关于右值引用、完美转发、引用折叠以及与此相关的所有内容的详尽详尽的文章,任何答案都是不完整的。

见这里:http ://thbecker.net/articles/rvalue_references/section_01.html

于 2013-05-30T13:57:45.627 回答
0

我会说是的,因为移动构造函数和移动赋值运算符现在是编译器为不定义/保护析构函数、复制构造函数或复制赋值的对象定义的。

这意味着如果您有以下代码...

struct intContainer
{
    std::vector<int> v;
}

intContainer CreateContainer()
{
    intContainer c;
    c.v.push_back(3);
    return c;
}

只需使用支持移动语义的编译器重新编译即可优化上面的代码。您的容器 c 将具有编译器定义的移动语义,因此将为 std::vector 调用手动定义的移动操作,而无需对您的代码进行任何更改。

于 2013-05-29T21:10:41.020 回答
-8

由于移动语义只适用于存在右值引用的情况,这些引用由新标记声明,,&&很明显它们是新的东西。

原则上,它们纯粹是一种优化技术,这意味着:1. 在分析器认为有必要之前不要使用它们,以及 2. 从理论上讲,优化是编译器的工作,移动语义不再是比必要的register

关于 1,我们可能最终会得到一个关于如何使用它们的普遍启发式方法:毕竟,通过 const 引用而不是通过值传递参数也是一种优化,但普遍的约定是传递类const 引用类型,所有其他类型的值。

关于 2,编译器还没有出现。至少,通常的那些。可以用来使移动语义无关的基本原则是(嗯?)众所周知的,但迄今为止,它们往往会导致实际程序的编译时间不可接受。

结果:如果您正在编写一个低级库,您可能需要从一开始就考虑移动语义。否则,它们只是额外的复杂性,应该被忽略,直到分析器另有说明。

于 2013-05-29T21:19:13.650 回答