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.
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.
"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 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.
rvalue
conversionThe 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.
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.
rvalue
is not an L-value referenceIn 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'
}
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With