5
votes

Can I simulate move constructor & move assignment operator functionality with copy constructor and assignment operator in C++98 to improve the performance whenever i know copy constructor & copy assignment will be called only for temporary object in the code OR i am inserting needle in my eyes?

I have taken two example's one is normal copy constructor & copy assignment operator and other one simulating move constructor & move assignment operator and pushing 10000 elements in the vector to call copy constructor.

Example(copy.cpp) of normal copy constructor & copy assignment operator

#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.
}

Example(move.cpp) of simulated move constructor & move assignment operator functionality with copy constructor & copy assignment operator

#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.
}

I observed performance is improved with some cost:

$ 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

We can observe that performance is increased with some cost.

  • Has any pitfall to implement move constructor and move assignment operator simulated functionality in c++98, even I am sure that copy constructor & assignment only call when temporary objects are created?
  • Has there any other way/technique to implement the move constructor and assignment operator in c++98?
1
[OT]: delete null pointer is noop, so the check is unneeded.Jarod42
@ Jarod42, yes, length assignment i missed it.Ajay yadav
Your const-cast is Undefined Behaviour. Your "Move Constructor" has no idea if it has been handed an actually const instance of a Memory Block. std::auto_ptr has something similar to move semantics, and that involves dealing with a constructor taking a non-const reference to the object.Andre Kostur
@AndreKostur The assignment may have undefined behaviour. (It's definitely poor style and unsafe code.) The cast itself does not.Asteroids With Wings

1 Answers

3
votes

You will not be able to have the language understand R-values in the same way that C++11 and above will, but you can still approximate the behavior of move semantics by creating a custom "R-Value" type to simulate ownership transferring.

The Approach

"Move semantics" is really just destructively editing/stealing the contents from a reference to an object, in a form that is idiomatic. This is contrary to copying from immutable views to an object. The idiomatic approach introduced at the language level in C++11 and above is presented to us as an overload set, using l-values for copies (const T&), and (mutable) r-values for moves (T&&).

Although the language provides deeper hooks in the way that lifetimes are handled with r-value references, we can absolutely simulate the move-semantics in C++98 by creating an rvalue-like type, but it will have a few limitations. All we need is a way to create an overload set that can disambiguate the concept of copying, from the concept of moving.

Overload sets are nothing new to C++, and this is something that can be accomplished by a thin wrapper type that allows disambiguating overloads using tag-based dispatch.

For example:


// 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);
}

We won't be able behave exactly like a reference by accessing members by the . operator, since that functionality does not exist in C++ -- hence having get() to get the reference. But we can signal a means that becomes idiomatic in the codebase to destructively alter types.

The rvalue type can be more creative based on whatever your needs are as well -- I just kept it simple for brevity. It might be worthwhile to add operator-> to at least have a way to directly access members.

I have left out T&& -> const T&& conversion, T&& to U&& conversion (where U is a base of T), and T&& reference collapsing to T&. These things can be introduced by modifying rvalue with implicit conversion operators/constructors (but might require some light-SFINAE). However, I have found this rarely necessary outside of generic programming. For pure/basic "move-semantics", this is effectively sufficient.

Integrating it all together

Integrating this "rvalue" type is as simple as adding an overload for rvalue<T> where T is the type being "moved from". With your example above, it just requires adding a constructor / move assignment operator:

    // 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
    }

This allows you to keep copy constructors idiomatic, and simulate "move" constructors.

The use can now become:

MemoryBlock mb(42);

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

Here's a working example on compiler explorer that compares the copy vs move assemblies.

Limitations

No PR-values to rvalue conversion

The one notable limitation of this approach, is that you cannot do PR-value to R-value conversions that would occur in C++11 or above, like:

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(); 

As far as I am aware, this cannot be replicated without language support.

No auto-generated move-constructors/assignment

Unlike C++11, there will be no auto-generated move constructors or assignment operators -- so this becomes a manual effort for types that you want to add "move" support to.

This is worth pointing out, since copy constructors and assignment operators come for free in some cases, whereas move becomes a manual effort.

An rvalue is not an L-value reference

In C++11, a named R-value reference is an l-value reference. This is why you see code like:

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

This named r-value to l-value conversion cannot be modeled without compiler support. This means that an rvalue reference will always behave like an R-value reference. E.g.:

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

Conclusion

So in short, you won't be able to have full language support for things like PR-values. But you can, at least, implement a means of allowing efficient moving of the contents from one type to another with a "best-effort" attempt. If this gets adopted unanimously in a codebase, it can become just as idiomatic as proper move-semantics in C++11 and above.

In my opinion, this "best-effort" is worth it despite the limitations listed above, since you can still transfer ownership more efficiently in an idiomatic manner.


Note: I do not recommend overloading both T& and const T& to attempt "move-semantics". The big issue here is that it can unintentionally become destructive with simple code, like:

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

This can cause buggy behavior in code, and is not easily visible. Using a wrapper approach at least makes this destruction much more explicit