[C++] What is copy_elision and why you should know about it

In the C++ world, copy_elision is a compiler optimization.

Prior to C++11, there was no copy_elision, so when you write code like this...

Person makePerson() {
    Person p();
    p.name = "Bob";
    return p;
}

... the compiler creates a Person object in the makePerson stack frame and is copied/moved to the caller's stack after makePerson finshes.

But with with copy_elision, the compiler optimizes the process and rather than a move/copy, compiler sets the address of makePerson's Person directly to the return address. In other words, there is no move/copy.

Explained by the official website https://en.cppreference.com/w/cpp/language/copy_elision

The objects are constructed directly into the storage where they would otherwise be copied/moved to.

This optimization is usually unnoticed.

Great! So what? Why do I need to care about this?

Tl;dr: When you lie to the compiler.

As a general rule: if you lie to the compiler, there lies bugs, undefined behavior, and probably stack smashing.

Let me tell a story about using ROS.

ROS creates a layer of abstraction for inter process communication (IPC) with sockets, in a publisher/subscriber model.

The ROS semantics for a subscriber is very overloaded, as in there are many definitions of "subscribe".

http://docs.ros.org/lunar/api/roscpp/html/classros_1_1NodeHandle.html

Here's what happened.

I defined an Object and created a callback. E.g. Cards object and callback_add_card(Card newCard).

Important: The callback returned an int. E.g. "int callback_add_card".
This leads me to make a mistake... We shall see why later.

I constructed the object and passed that object's callback to ROS subscribe.

But ROS's subscribe definition expected (void*) function pointer.
I.e. void callback_add_card was acceptable but int callback_add_card is not.
So I lied to the compiler and cast this to a void*. Subtle mistake. Won't show up until later.

#define callback_add_card_fn (void (Cards::*)(const Card&))

nodeHandle.subscribe("/cards/add", subscribeQueueSize, callback_add_card_fn &Cards::callback_add_card, &my_cards);

The compiler was happy and compiled. Because the "#define cb_car_speed ..." told the compiler to treat the function as void.

Code mostly worked fine.

Because an int is relatively small (byte), there appears to be no bugs because the stack smashing wasn't obvious.

Sometime later, I changed the return type.
From int callback_add_card to Card callback_add_card.
Because why not just return the Card instead? So I can write some test code and see if the Card is correct.

Well! Now we have big trouble.

The compiler was told this callback_add_card had void return type. So compiler did not allocate any space for a return value. And because of copy_elison, the compiler had generated code for callback_add_card "directly into the storage where they would otherwise be copied/moved to."

In other words, calling callback_add_card smashes the stack of the caller. Because the caller expected void return value, and no spaces was allocated. But the copy_elison code constructed the Card object in the caller's stack frame!

So how do we get around this? How to have a callback that returns some value, but still call the ROS subscribe properly?

Answer: By wrapping the call in a lambda, creating a closure. The lambda returns no value, so it's a void function. But the lambda can capture context. E.g.

nodeHandle.subscribe<Card>("/cards/add", subscribeQueueSize, [&my_cards](Card newCard) {
  cout << "the card added is: " << my_cards.callback_add_card() << "\n";
});

Leave a Reply

Your email address will not be published. Required fields are marked *