【问题标题】:C++ 11 move semantics vs C++ 98C++ 11 移动语义与 C++ 98
【发布时间】:2014-08-16 21:19:02
【问题描述】:

我研究过 c++ 11 移动语义,我有这样一个问题。

例如:

如果我们有一个

vector<T> vt; // also assume that T have pointers on data in separate memory
vt.push_back(...);

假设vt 缺少未使用的容量。然后在 c++98 中,它将为每个 T 对象分配更多的内存和副本(调用构造函数副本)以及它们指向的数据。

例如:

t1 -> t1 数据 => 将被复制 t_cop1 -> (t1_cop1 数据) t2 -> t2 数据 => t_cop2 -> (t2_cop2 数据)

C++ 11 移动语义允许移动所有 T 对象(只需复制一个指针,但不要通过调用移动构造函数将数据复制到单独的内存中)。

所以问题是为什么我们不能在 C++98 中做同样的事情,为什么我们不能实现 push_back 只是为了复制内存(其中包含指针 t1、t2,因此 t_cop1 和 t2_cop2 将使用相同的指针创建)和之后释放它(例如 t1 和 t2 使用 free(void*))?

UPD:好的,我会尝试用更简单的例子来解释我的问题:

例如,如果我们有一个包含指向某个数据结构 Data 的指针的类 A。 我们实现了一个复制指针“数据”的复制构造函数

class A {
public:
    A() {};
    A(const A& a) {
        data = a.data;
    }
private:
    struct Data{};
    Data* data;
};

// And I want to implement "move semantics" by calling the copy constructor (that copies just pointer)

A* a = new A();
A* b(a);

// and then somehow free "a" object (but not call the destructor)
free(a); // or a = NULL

据我所知,这就是移动语义几乎所做的(但让 a 处于某种一致的状态)。所以我认为我们实际上有能力在旧的 C++ 中实现移动。不是吗?

我们也可以在前面的例子中用向量做同样的事情

【问题讨论】:

  • 我认为你误解了移动语义......
  • 这里 C++11 和 C++98 没有区别,因为移动指针时指针只是被复制。还有一个vector&lt;T&gt;,其中T是一个指针,它只会在指针周围移动,而不会在指针指向的地方移动。
  • 执行,但 C++ 98 将调用深度复制,因此它不仅会复制指针,还会复制指针指向的所有数据。所以问题是为什么我们不能用 C++98 实现移动语义
  • @Nikita 不,c++98 不会自动进行深度复制。它只会复制指针值,就像 c++11 一样。
  • 这个:open-std.org/jtc1/sc22/wg21/docs/papers/2005/… 解释了为什么 auto_ptr 不起作用。

标签: c++ move move-semantics


【解决方案1】:

“移动语义”为编译器提供了一种方法来传递对构造函数之类的引用,这些引用可以区分为左值引用(您习惯使用的那种,在 C++98 中找到)和右值引用(C++11 新增,使用&amp;&amp; 表示法,这些引用临时和中间对象,并且可能不会持续超过当前语句)。

所以现在我们可以编写一个复制构造函数(“让新对象成为这个对象的副本”)和一个不同的移动构造函数(“让新对象成为这个对象的副本,哦顺便说一句,源对象是下一个将被销毁的临时对象,因此,如果您想以一种不会使源对象保持完整的方式进行优化,请继续”)。

因此,例如,在容器类中,您可以使用复制构造函数复制容器类,新副本将包含其所有内容的完整副本(这意味着像内容会发生)。但是移动构造函数可以跳过所有工作,只需与源容器交换内部结构,只需接管源容器的内容并将源容器留空。 (后来源容器被销毁,这应该是微不足道的,因为它是空的。)这通常可以在恒定时间内完成,而不是 O(N) 或更糟。

您不能在 C++98 中将等效的作为构造函数(因此使用临时对象),因为它只知道如何制作副本。有一种方法可以通过swap() 函数(以及某些类中的方法)实现类似的效果,但必须明确编写。 (基本上,要将容器a 的内容移动到新构建的空容器b 中,您可以调用swap(a, b);,然后销毁a,这将是空的。)


在 C++98 中不能做正确事情的一个例子是使用返回向量的函数的结果初始化向量:

vector<int> fibonacci(int n) { 
    vector<int> result;
    if(n <= 0) return result;
    result.push_back(0);
    if(n <= 1) return result;
    result.push_back(1);
    for(int i = 1; i < n; ++i)
        result.push_back(result[i] + result[i - 1]);
    return result; 
}

vector<int> datavect = fibonacci(10);

C++98 和 C++11 都将省略函数返回副本(在存储返回值的位置创建 result)但 C++98 只能调用 datavect 的复制构造函数因为没有移动语义。 C++11 可以改为调用移动构造函数,它可以将存储从临时返回值移动到datavect。如果不涉及 operator newoperator delete 并返回指针而不是对象,那么在 C++98 中没有真正的方法可以做到这一点,在(非确定性时间)免费存储上毫无收获。

【讨论】:

  • 是的,这就是移动语义所做的。所以我的问题是我们如何(或为什么不)不能在 C++ 98 中实现移动语义?
  • 见我的最后一段。简而言之:您必须手动完成,并且在某些情况下根本无法完成,例如返回容器的函数(而不是指针或引用)。
  • 是的,我同意在交换和斐波那契的情况下,旧的 c++ 是不合适的。但在我的示例中(也使用向量),可以通过模拟移动语义来提高性能。那么为什么他们在 C++11 之前没有在 stl 中实现呢?
  • @Nikita 问题不是 STL,而是用户定义的类型。对于需要这种优化的所有值类型的容器,您将需要这种奇怪的行为(复制 == 资源转移)。本质上就像std::auto_ptr。然而,这打破了通常的复制语义,其中复制(按照约定)意味着深度复制。所以你可以在容器中使用这样的类型作为值类型,但是你不能真正在容器外使用相同的类型。您可以更改 STL 并引入新的 ctor,例如 A(A&amp;, transfer_resource_t),但这不适用于右值。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-01-02
  • 1970-01-01
  • 2014-03-18
  • 2023-03-26
  • 1970-01-01
  • 2012-07-05
  • 1970-01-01
相关资源
最近更新 更多