c++ - 在 C++98 中实现移动构造函数和移动赋值运算符以获得更好的性能

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

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

我举了两个例子,一个是普通的复制构造函数和复制赋值运算符,另一个是模拟移动构造函数和移动赋值运算符,并在 vector 中推送 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 中的赋值运算符?
  • 最佳答案

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

    该方法

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

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

    重载集对 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->至少有一种方法可以直接访问成员。

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

    将这一切整合在一起

    集成这个“右值”类型就像为 rvalue<T> 添加一个重载一样简单。哪里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比较复制与移动程序集。

    限制

    没有 PR 值到 rvalue转换

    这种方法的一个显着限制是,您不能进行 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));
    }
    

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

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

    结论

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

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

    注:我不建议同时重载 T&const T&尝试“移动语义”。这里的大问题是,它可能会在使用简单代码时无意中变得具有破坏性,例如:

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

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

    关于c++ - 在 C++98 中实现移动构造函数和移动赋值运算符以获得更好的性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34607800/

    相关文章:

    c++ - 嵌套 vector 深度为 3 或以上的编译器警告

    java - 调用 set 方法而不调用构造函数

    java - 当构造函数需要调用可重写方法时保持干燥

    c++ - 关联容器 (STL) 中的平等评估

    c++ - 有效地将大复数 vector 乘以标量 C++

    r - 如何从R中的1行表转换向量

    python - Pygame Sprite 匀速移动

    c++ - 为什么 std::get_deleter() 总是产生 nullptr

    c++ - 向 std::map 添加一个没有值的键

    C++ 构造函数设置值