Move semantics, introduced in C++11, is a powerful feature that allows to move data instead of copying it. Is it that simple? Of course not, C++ has to be complicated!.
I like to share this image, but the hidden truth is that C++ makes me cry too š.
Move semantics: std::move
and Foo&&
, thatās all right?
I knew that move doesnāt really move, and I hated the C++ compiler not raising an error when one does std::move
and it results in a copy instead š.
But until now I almost mechanically used std::move
and Foo&&
when I needed to move something. And I knew that I had to move after doing a move:
struct Foo
{
Foo() = default;
Foo(const Foo&) { std::cout << "copy ctor\n"; }
Foo(Foo&&) { std::cout << "move ctor\n"; }
};
struct Bar
{
Bar(Foo&& foo) : m_foo(std::move(foo)) {}
private:
Foo m_foo;
};
I thought that was because Foo&& foo
was an rvalue, but became an lvalue when passed to m_foo
, so it had to be moved again.
Today, almost twelve years after C++11 was realeased, I found out that move can (should?) go without the rvalue reference. Youād say: isnāt it going to make a copy? Nope š¤Ø.
To the code!
I wrote a simple program to test it, leveraging the caracteristics of std::unique_ptr
.
It consists of a producer, a consumer, and a borrower.
To begin with, we define a Data
class, which is a simple wrapper around a std::string
:
struct Data
{
Data(const std::string& datum)
: m_datum(datum)
{
std::cout << "[" << this << "] Data::Data()" << std::endl;
}
~Data() { std::cout << "[" << this << "] Data::~Data()" << std::endl; }
std::string_view datum() const { return m_datum; }
private:
std::string m_datum;
};
Then we define the consumer and the borrower:
struct Consumer
{
void consume(std::unique_ptr<Data> data)
{
std::cout << "[CONSUMER] got the data: Ā«" << data->datum() << "Ā»" << std::endl;
}
};
struct Borrower
{
void borrow(std::unique_ptr<Data>&& data)
{
std::cout << "[BORROWER] got the data: Ā«" << data->datum() << "Ā»" << std::endl;
// I might steal
if (m_moocher)
{
// and I will!
auto data_is_mine_now = std::move(data);
}
}
void setBadWill() { m_moocher = true; }
private:
bool m_moocher { false };
};
Note void consume(std::unique_ptr<Data> data)
as opposite to void borrow(std::unique_ptr<Data>&& data)
, and mind that unique_ptr
canāt be copied!
Isnāt it weird that consume()
takes a unique_ptr
by value š¤?
Letās see the producer:
struct Producer
{
Producer(Borrower& borrower, Consumer& consumer)
: m_borrower(borrower)
, m_consumer(consumer)
{
}
void produce()
{
std::cout << "[PRODUCER] producing some data..." << std::endl;
m_data = std::make_unique<Data>("Hello, World!");
std::cout << "[PRODUCER] lending the data..." << std::endl;
m_borrower.borrow(std::move(m_data));
std::cout << "[PRODUCER] data should be mine again, checking... "
<< std::boolalpha << (m_data != nullptr) << std::endl;
if (!m_data)
{
std::cout << "[PRODUCER] what a dishonest borrower! "
<< "If I'll give m_data to the consumer, it will crash!"
<< std::endl;
}
else
{
m_consumer.consume(std::move(m_data));
}
std::cout << "[PRODUCER] m_data should be nullptr now, checking... "
<< std::boolalpha << (m_data == nullptr) << std::endl;
}
private:
std::unique_ptr<Data> m_data;
Borrower& m_borrower;
Consumer& m_consumer;
};
Now we begin to understand whatās going on.
Foo&&
isnāt called ārvalue referenceā just because Foo&
is āreferenceā, it is a true reference.
When you have an rvalue reference you may or may not take the ownership of the data, and Iām exercising that with Borrower::setBadWill()
.
Long story short, we can have two std::move(m_data)
without any idea whether the data will be moved or not.
Yay š¤”!
For the sake of completeness, hereās the main()
:
int main()
{
Borrower borrower;
Consumer consumer;
std::cout << "\n=== First run ===" << std::endl;
Producer producer(borrower, consumer);
producer.produce();
std::cout << "\n=== Second run ===" << std::endl;
borrower.setBadWill();
producer.produce();
return 0;
}
and its output:
=== First run ===
[PRODUCER] producing some data...
[0x55dcee0616c0] Data::Data()
[PRODUCER] lending the data...
[BORROWER] got the data: Ā«Hello, World!Ā»
[PRODUCER] data should be mine again, checking... true
[CONSUMER] got the data: Ā«Hello, World!Ā»
[0x55dcee0616c0] Data::~Data()
[PRODUCER] m_data should be nullptr now, checking... true
=== Second run ===
[PRODUCER] producing some data...
[0x55dcee0616c0] Data::Data()
[PRODUCER] lending the data...
[BORROWER] got the data: Ā«Hello, World!Ā»
[0x55dcee0616c0] Data::~Data()
[PRODUCER] data should be mine again, checking... false
[PRODUCER] what a dishonest borrower! If I'll give m_data to the consumer, it will crash!
[PRODUCER] m_data should be nullptr now, checking... true
An youāll find the complete code here to play with. Have fun!