Move Semantics
Move semantics are an essential cpp concept. But to understand and appreciate them, we need to understand certain basics first.
lvalue and rvalue⌗
int i = 0;
Here:
i
is an lvalue whereas0
is an rvalue.- lvalue is an object that occupies some identifiable location in memory.
- rvalue is an object that does not occupy some identifiable location in memory.
Basics of lvalue and rvalue⌗
- We can assign an lvalue to an rvalue. But not vice-versa
For example:
int i = 10;
10 = i; // non-sense
- We can’t have an lvalue reference of an rvalue, But having an const lvalue reference of an rvalue is fine
int& i = 10; // Error
const int& i = 10; // OK
- We can’t have an rvalue reference of an lvalue
int i = 10;
int&& j = i; // Error
Functions and lvalue, rvalue⌗
The return value of a function is an l-value if and only if it is a reference
int Get()
{
return 5;
}
i = Get();
Get() = i; // Error
just to make it extra clear, it doesn’t really matter if you change to:
int Get()
{
int i = 5;
return i;
}
Again, only way to return an lvalue is by returning an lvalue reference.
int& Get()
{
int a = 1;
return a;
}
Now you can do this:
i = Get();
Get() = i;
Bonus:
int& Get()
{
static int a = 1;
return a;
}
Can only have an lvalue refrence of an lvalue
void Set(int& i)
{
i = 5;
}
int i = 10;
Set(i); // OK
Set(10); // Error
Set(i+ 10); // Error
No location to store this value, so we can’t have an lvalue reference of an rvalue
const lvalue reference are fine however, because they can bind to rvalues
void Set(const int& i)
{
printf("const lvalue reference\n");
}
Move Semantics⌗
What is MOVING?⌗
Moving in cpp means to transfer the ownership of an object from one variable to another. It is a very fast operation, because it doesn’t involve any copying of data. It is just a pointer copy. Moving isn’t cpoying and deleting the old object. It is just transferring the ownership of the object from one variable to another.
Copying
X --> 0x16af33548
X --> 0x16af33548 0x16d473548 <--Y
Moving
X --> 0x16af33548
X --> NULL 0x16af33548 <--Y
No malloc, no construction
Move Constructor⌗
class vector
{
private:
int* m_data;
int m_size;
public:
vector() = default;
vector(int* data, const int& size) : m_size(size)
{
m_data = new int[m_size];
memcpy(m_data, data, m_size * sizeof(int));
printf("Vector contructed\n");
}
vector(const vector& other) : m_size(other.m_size)
{
m_data = new int[m_size];
memcpy(m_data, other.m_data, m_size * sizeof(int));
printf("Vector copied\n");
}
vector(vector&& other) : m_size(other.m_size)
{
m_data = other.m_data;
m_size = other.m_size;
// Make other point to null
other.m_data = nullptr;
other.m_size = 0;
printf("Vector moved\n");
}
int size() const { return m_size; }
int& operator[](const int& index) { return m_data[index]; }
};
int main()
{
int data[] = { 1, 2, 3, 4, 5 };
vector v1(data, 5);
vector v2 = v1; // Copy constructor
vector v3 = std::move(v1); // Move constructor
std::cout<< v1.size(); // 0
return 0;
}
Important Note: now v1
points to nullptr
and v3
points to v1
’s data. So if we try to access v1
’s data, we will get a segmentation fault.
Performance: Ofcoure it would be fast, memory alloc is often the bottleneck.
int main()
{
int size = 1000;
int* data = new int[size];
for (int i = 0 ; i < size ; ++i)
{
data[i] = i;
}
vector v = vector(data, size);
auto time1 = high_resolution_clock::now();
for (int i = 0 ; i < 10000000 ; i++)
{
foo temp2 = foo(v);
}
auto time2 = high_resolution_clock::now();
for (int i = 0 ; i < 10000000 ; i++)
{
foo temp2 = foo(std::move(v));
}
auto time3 = high_resolution_clock::now();
printf("No move, no inline construction: %d\n", duration_cast<microseconds>(time2 - time1).count());
printf("Move, no inline construction : %d\n", duration_cast<microseconds>(time3 - time2).count());
return 0;
}
Output:
No move, no inline construction: 10116165
Move, no inline construction : 83041
Perfect Forwarding⌗
Goal: To make a function take values and forward them to another with retaining them as lvalues or rvalues
template<typename T, typename Args>
T Create(Args& a)
{
return T(a);
}
int i = 10;
int temp1 = Create(i); // OK
int temp2 = Create(10); // Error
int temp3 = Create(i+ 10); // Error
Maybe, overlord it with const Args&
?
template<typename T, typename Args>
T Create(const Args& a)
{
return T(a);
}
Now this works
int i = 10;
int temp1 = Create(i); // OK
int temp2 = Create(10); // OK
int temp3 = Create(i+ 10); // OK
But it’s not forwarding, it’s just taking a copy of the value
class foo
{
public:
int m_i;
public:
foo(int i) : m_i(i)
{
printf("foo(int i)\n");
}
};
int main()
{
int i = 10;
foo temp1 = Create<foo>(i); // OK
foo temp2 = Create<foo>(10); // OK, but it constructs an object for an rvalue, now it is no longer an rvalue
return 0;
}
For example if we have a constructor that takes an rvalue reference
class foo
{
public:
int m_i;
public:
foo(int&& i) : m_i(i)
{
printf("foo(int&& i)\n");
}
};
Now we get a compiler error. Because we are passing an lvalue to a function that takes an rvalue reference
Solution: Forwarding
template<typename T, typename ... Args>
T Create(Args&& ... a)
{
return T(std::forward<Args>(a) ... );
}
int main()
{
int i = 10;
std::cout << "i = " << &i << std::endl;
foo temp1 = Create<foo>(i); // OK
foo temp2 = Create<foo>(10); // OK and now no construction!!!
return 0;
}