【问题标题】:implement move constructor & move assignment operator in c++98 for better performance在 c++98 中实现移动构造函数和移动赋值运算符以获得更好的性能
【发布时间】:2016-01-05 09:07:34
【问题描述】:

我是否可以在 C++98 中使用复制构造函数和赋值运算符来模拟移动构造函数和移动赋值运算符功能,以提高性能,只要我知道复制构造函数和复制赋值将只为代码中的临时对象调用或者我正在插入针在我眼里?

我举了两个例子,一个是普通的复制构造函数和复制赋值运算符,另一个是模拟移动构造函数和移动赋值运算符并在向量中推送 10000 个元素来调用复制构造函数。

普通拷贝构造函数和拷贝赋值运算符的例子(copy.cpp)

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

class MemoryBlock
{
public:

   // Simple constructor that initializes the resource.
   explicit MemoryBlock(int length)
      : _length(length)
      , _data(new int[length])
   {
   }

   // Destructor.
   ~MemoryBlock()
   {

      if (_data != NULL)
      {
         // Delete the resource.
         delete[] _data;
      }
   }


//copy constructor.
MemoryBlock(const MemoryBlock& other): _length(other._length)
      , _data(new int[other._length])
{

      std::copy(other._data, other._data + _length, _data);
}

// copy assignment operator.
MemoryBlock& operator=(MemoryBlock& other)
{
  //implementation of copy assignment
}

private:
   int  _length; // The length of the resource.
   int*  _data; // The resource.
};


int main()
{
   // Create a vector object and add a few elements to it.
   vector<MemoryBlock> v;
   for(int i=0; i<10000;i++)
   v.push_back(MemoryBlock(i));

   // Insert a new element into the second position of the vector.
}

使用复制构造函数和复制赋值运算符模拟移动构造函数和移动赋值运算符功能的示例(move.cpp)

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;

class MemoryBlock
{
public:

   // Simple constructor that initializes the resource.
   explicit MemoryBlock(int length=0)
      : _length(length)
      , _data(new int[length])
   {
   }

   // Destructor.
   ~MemoryBlock()
   {

      if (_data != NULL)
      {
         // Delete the resource.
         delete[] _data;
      }
   }


// Move constructor.
MemoryBlock(const MemoryBlock& other)
{
   // Copy the data pointer and its length from the 
   // source object.
   _data = other._data;
   _length = other._length;
   // Release the data pointer from the source object so that
   // the destructor does not free the memory multiple times.
   (const_cast<MemoryBlock&>(other))._data  = NULL;
    //other._data=NULL;
}

// Move assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
   //Implementation of move constructor
   return *this;
}

private:
   int  _length; // The length of the resource.
   int*  _data; // The resource.
};


int main()
{
   // Create a vector object and add a few elements to it.
   vector<MemoryBlock> v;
   for(int i=0; i<10000;i++)
   v.push_back(MemoryBlock(i));

   // Insert a new element into the second position of the vector.
}

我观察到性能有所提高,但需要付出一些代价:

$ g++ copy.cpp -o copy
$ time ./copy 
real    0m0.155s
user    0m0.069s
sys 0m0.085s

$ g++ move.cpp -o move
$ time ./move 
real    0m0.023s
user    0m0.013s
sys 0m0.009s

我们可以观察到性能的提高需要一些成本。

  • 实现移动构造函数和移动赋值有任何缺陷 运算符在 c++98 中模拟功能,即使我确信复制 构造函数和赋值仅在临时对象存在时调用 创建?
  • 有没有其他方法/技术来实现移动构造函数 和 c++98 中的赋值运算符?

【问题讨论】:

  • [OT]:删除空指针是noop,所以不需要检查。
  • @Jarod42,是的,我错过了长度分配。
  • 你的 const-cast 是未定义的行为。您的“移动构造函数”不知道它是否已收到内存块的实际 const 实例。 std::auto_ptr 有一些类似于移动语义的东西,这涉及到处理一个构造函数,该构造函数对对象进行非常量引用。
  • @AndreKostur 作业可能有未定义的行为。 (这绝对是糟糕的风格和不安全的代码。)演员表本身没有。

标签: c++ vector constructor copy-constructor c++98


【解决方案1】:

您将无法让该语言以与 C++11 及更高版本相同的方式理解 R 值,但您仍然可以通过创建自定义“R 值”来近似 move 语义的行为键入以模拟所有权转移。

方法

“移动语义”实际上只是以一种惯用的形式破坏性地编辑/窃取对象引用中的内容。这与从不可变视图复制到对象相反。在 C++11 及更高版本的语言级别引入的惯用方法以重载集的形式呈现给我们,使用 l 值表示副本 (const T&amp;),使用 (可变) r 值表示移动 (T&amp;&amp;) .

尽管该语言在使用 r 值引用处理生命周期的方式中提供了更深层次的钩子,但我们绝对可以通过创建 rvalue-like 来模拟 C++98 中的移动语义类型,但它会有一些限制。我们所需要的只是一种创建重载集的方法,该方法可以消除复制概念和移动概念的歧义。

重载集对 C++ 来说并不是什么新鲜事,这可以通过一个瘦包装器类型来实现,它允许使用基于标签的调度来消除重载的歧义。

例如:


// A type that pretends to be an r-value reference
template <typename T>
class rvalue { 
public:
    explicit rvalue(T& ref) 
        : _ref(&ref)
    {

    }

    T& get() const {
        return *_ref;
    }

    operator T&() const {
        return *_ref;
    }

private:
    T* _ref; 
};

// returns something that pretends to be an R-value reference
template <typename T>
rvalue<T> move(T& v)
{
    return rvalue<T>(v);
}

通过. 运算符访问成员,我们将无法完全像引用一样行事,因为 C++ 中不存在该功能 - 因此有 get() 来获取引用.但我们可以发出一种在代码库中变得惯用的方法来破坏性地改变类型。

rvalue 类型也可以根据您的需求更具创意——为了简洁起见,我只是保持简单。添加operator-&gt; 以至少有一种直接访问成员的方法可能是值得的。

我忽略了T&amp;&amp; -> const T&amp;&amp; 转换,T&amp;&amp;U&amp;&amp; 转换(其中UT 的基础),并且T&amp;&amp; 引用折叠到T&amp;。这些东西可以通过使用隐式转换运算符/构造函数修改rvalue 来引入(但可能需要一些轻量级的 SFINAE)。但是,我发现这在泛型编程之外很少需要。对于纯/基本的“移动语义”,这实际上就足够了。

将它们整合在一起

集成这个“右值”类型就像为rvalue&lt;T&gt; 添加一个重载一样简单,其中T 是被“移出”的类型。使用上面的示例,它只需要添加一个构造函数/移动赋值运算符:

    // Move constructor.
    MemoryBlock(rvalue<MemoryBlock> other)
        : _length(other.get()._length),
          _data(other.get()._data)
    {
        other.get()._data = NULL;
    }

    MoveBlock& operator=(rvalue<MemoryBlock> other)
    {
        // same idea
    }

这允许您保持复制构造函数的惯用性,并模拟“移动”构造函数。

现在可以变成:

MemoryBlock mb(42);

MemoryBlock other = move(mb); // 'move' constructor -- no copy is performed

这是一个working example on compiler explorer,用于比较复制与移动程序集。

限制

rvalue 转换没有 PR 值

这种方法的一个显着限制是,您不能执行 C++11 或更高版本中会发生的 PR 值到 R 值的转换,例如:

MemoryBlock makeMemoryBlock(); // Produces a 'PR-value'

...

// Would be a move in C++11 (if not elided), but would be a copy here
MemoryBlock other = makeMemoryBlock(); 

据我所知,如果没有语言支持,这是无法复制的。

没有自动生成的移动构造函数/赋值

与 C++11 不同,不会有自动生成的移动构造函数或赋值运算符 - 因此对于要添加“移动”支持的类型,这将成为手动操作。

这一点值得指出,因为在某些情况下复制构造函数和赋值运算符是免费提供的,而移动则需要手动操作。

rvalue 不是 L 值引用

在 C++11 中,命名的 R 值引用左值引用。这就是为什么您会看到如下代码:

void accept(T&& x)
{
    pass_to_something_else(std::move(x));
}

如果没有编译器支持,这种命名的右值到左值的转换无法建模。这意味着rvalue 引用将始终表现得像一个 R 值引用。例如:

void accept(rvalue<T> x)
{
    pass_to_something_else(x); // still behaves like a 'move'
}

结论

因此,简而言之,您将无法获得对 PR 值等内容的完整语言支持。但是您至少可以实现一种方法,允许通过“尽力而为”的尝试有效地将内容从一种类型移动到另一种类型。如果这在代码库中被一致采用,它会变得像 C++11 及更高版本中的正确移动语义一样惯用。

在我看来,这种“尽力而为”是值得的,尽管存在上述限制,因为您仍然可以以惯用的方式更有效地转让所有权。


注意:我确实建议重载T&amp;const T&amp; 来尝试“移动语义”。这里的大问题是它可能会无意中通过简单的代码变得具有破坏性,例如:

SomeType x; // not-const!
SomeType y = x; // x was moved?

这可能会导致代码出现错误行为,并且不容易看到。使用包装器方法至少使这种破坏更加明确

【讨论】:

  • 引导说明的示例为 auto_ptr
  • 我不确定谁否决了这个解决方案,但愿意提供解释以便我处理反馈吗?
猜你喜欢
  • 2020-12-03
  • 2016-05-19
  • 2017-05-15
  • 1970-01-01
  • 2013-06-11
  • 2017-01-16
  • 2020-09-06
  • 2015-07-10
  • 2021-08-18
相关资源
最近更新 更多