From Java to C++ – RAII and parameter passing with copy&move semantics
In my opinion, in Java we do not care that much about object creation and then assigning them to other variables. In general, due to GC inner-workings, we’re more reluctant in this area, than C++ programmers. However, that’s not the only reason why I’ve decided to write this post. Recently a survey result appeared in my social feed claiming, that move semantics and understanding object lifecycle is not a piece of cake. In order to cover all my bases, I’ve decided to dive into the topic a little deeper. However, that is not the only thing that I have to present. In general, after writing a first version of this article, my C++ coach showed me so many places in which I was wrong, that I’ve rewritten everything. As a result, this post got really long, but right now it contains all the info that every Java programmer coming to C++ should have. Here we go.
Memory usage in Java and C++
In order to fully understand what’s going on in this article, we have to discuss first the memory model used in both these languages. Both of them share the same concepts – the stack and the heap, although their usage differs.
In Java, the stack is used during program execution, to handle calls to the methods by storing stack frames. These frames contain information about the method being called, a return address (method that called it), references and primitive values. Every time there’s an exception thrown, we can actually see a stack-trace, a long list of the steps that we’ve executed.
What is important here, is the fact, that the stack in Java does not contain actual objects! Every time we execute code like this:
void someMethod() { String stringRef = "TestStringAllocatedOnTheHeap"; System.out.println(s); }
A reference stringRef is allocated on the stack, however an actual value of this reference, is kept in the heap. When we leave the method, the stringRef goes out of scope, and the stack is cleared (its pointer is just moved backwards). So our reference is destroyed, and the allocated value is left on the heap, and will be garbage-collected in the future (I leave the subject of a string-pools in Java for now).
For the sake of completeness, I have to mention primitive values. They’re always allocated using the stack, and they go out of scope (and memory), as soon as the stack is cleared.
C++ uses stack in quite similar way – it traces every function call, stores information about the passed variables and so on. However, there’s one difference here – it is possible to use stack to store variables of any kind! In Java, we can use stack to store primitives and references. In C++, we can actually store whatever we want. As in this simple example:
void testFunction() { SomeBigCollectionOfInts<int> myCollection(10); // do whatever we want with the Vector instance }
Here, myCollection is a variable that is allocated on the stack, and that means that both the ‘reference’, and an actual values are on it. We’re not touching the heap whatsoever! It is a very fundamental difference between Java and C++, so please take note here! Why is this better in C++ to use stack to keep our variables? The answer should be simple now – it’s way faster than any kind of dynamic memory access (using the heap).
Reference semantics vs value semantics with parameters passing
Reference semantics in Java
That were the basics of memory usage. Let’s talk right now about parameters passing, as this is another place, where Java and C++ differ. In Java, we have this strange dualism, when it comes to the value/reference semantics. Let’s start with this simple code:
class MyObject { int i = 5; String s = "test"; } MyObject obj1 = new MyObject(); MyObject obj2 = obj1; obj1.i = 9; System.out.println("Obj1: " + obj1.i); System.out.println("Obj2: " + obj2.i);
Will print: Obj1: 9 Obj2: 9
What is happening here? By creating first instance of MyObject, we allocate some memory, and we put a specific values there. It looks like this:
Actual value of the variable obj1 is not an object with property i or s. What we actually have, is a reference or pointer to the specific address in memory, which we can use (using ‘.’) to fetch data that is stored there.
When we create a second object, and we assign to it the existing one, we do not create a copy of it, we just create a copy of the pointer value (of the address that stores our data)! The situation looks like this:
As we’re pointing to the same part of memory, whatever change is done to it, will be reflected in all the pointers, that are pointing to this specific memory address. That’s how Java is working from the very beginning. We pass references, not values!
If you have any familiarity with Java, you should stop me right here. Ok buddy, what about primitives? Yup, you got me. Primitive values – notice the word values – are different. They’re always passed by value, therefore for every method call including primitives, we get a brand-new copy. No references pointing to God knows where, just an in-place instance. Let’s take a look:
private static void test(int a) { a = 6; System.out.println("Inside method: " + a); } public static void main(String args[]) { int a = 2; test(a); System.out.println("In main method: " + a); }
Output being:
Inside method: 6 In main method: 2
That’s Java. That’s reference semantics. We’re covered. However, I’m trying to explain C++ here, not Java. So how is it done in C++? Well, differently…
Default value semantics in C++
I won’t go into the details of pointers in C++ at the moment – no matter if we discuss raw pointers or smart ones. There will be a time for them in the future posts. Right now let’s answer the simple question – how are parameters passed in C++? It’s actually simple – the default given to us by the language is copy semantics. By default, whenever we pass an argument to the function, we pass it by value.
#include<iostream> void testFunction(std::string type, int amount) { type = "New value"; amount = 6; std::cout << type << " " << amount << std::endl; } int main() { std::string type = "Start value"; int startAmount = 5; std::cout << type << " " << startAmount << std::endl; testFunction(type, startAmount); std::cout << type << " " << startAmount; }
The output is not surprising:
Start value 5 New value 6 Start value 5
All the parameters (and notice, that std::string is not a built-in type, like int for example), were passed by value. Whatever changes were done inside the function, they’re not seen outside it. It’s like Java primitives passing – everything goes by value. The syntax is clear, and output is the same as well. The question arises – isn’t that doing too much job under the hood? Creating an actual copy takes time and memory, so why that’s the default behaviour? Why was it chosen to be the default?
Remember above Java example? And its duality when it comes to references, and primitive values? That sort of ambiguity is fine, as long as you have garbage collector under the hood to take care of the objects/references passed around. In C++ we don’t have a GC at our disposal, and it’s the programmer’s job to make sure that the memory is not leaking (and other resources too – more on that later – but for the sake of brevity I will use memory as an example). However, what I want to point here, is that every reference is just a pointer to some data in memory. When this pointer is passed around, it may be reassigned to point to a different place in memory. Unless the pointed-to memory was freed before the reassignment, that will result in memory leak, as the allocated memory, that was pointed to, right now is abandoned. In all GC-based languages that is not a problem, with the next GC cycle these objects will be freed, and our problems are gone. In C++ we don’t have that comfort, so assuring that memory is freed when it can, is a critical task for the programmer.
The best way to avoid all that trouble, is to use value semantics. Why? There are four main reasons for that:
- fast memory access – whenever we pass objects by value, we’re using a stack to keep these values. A great thing about the stack is, that it is amazingly fast. When variable is allocated on the stack, it is one assembly instruction, that just puts the value on it, and moves the stack pointer by the specified size forward. Comparing it to any kind of operation using references/pointers access, is like comparing a cheetah to a turtle.
- simplified memory management – as long as a values goes out of the scope – in this example the function returns – its value is discarded. As simple as that. We don’t have to think about it at all.
- self-expressiveness – values cannot be empty! Remember Java, and its never-changing worry, that passed parameter is actually NULL. Optionals, Groovy/Kotlin syntax to prevent that – all the is just sugar-coating the fact, that references can be dangerous. With a plain value – not a problem.
- thread safety
- no reference aliasing issues – when seeing code like
void foo(MyObject a, MyObject b)
we are sure that we have two different objects, not (possibly) references to the same one
If that is not enough, I recommend reading this SO question, which gives a comprehensive lecture about copy elision and return value optimisation.
Value semantics and RAII
As I’ve mentioned, we may have a lot of other resources allocated in our classes – mutexes, file handles, GUI elements to just name a few. It’s always a problem while passing references/pointers to such classes around – who is actually responsible for handling those resources? If there’s an error/exception in my function that uses such object, should I free/release resources then? Or should it be the holding class’ job?
Here the pattern called RAII (Resource acquisition is initialization) is our guy. The general idea behind it is simple – whatever resource is acquired by the class in the constructor, must be deallocated/freed/unlocked in the destructor. Such resource (be it mutex or dynamic memory) becomes a class invariant, and therefore acquiring it is a necessary requirement for an object creation. Here’s a conceptual example:
struct MyObject { SomeResource someResource; MyObject() { // acquire the resource, if not - error/exception } ~MyObject() { // release the resource } }
With such construction, resource allocation is bound to the object lifetime. That’s why passing an object by value simplifies things – we have an object coming to our function, and it is being destroyed while the function does its job (object goes out of scope). With object destruction, any kind of resources that were allocated by it, are released as well. We don’t have to wonder about the internal state of the passed parameter. It’s its author (and compiler) job to handle this.
That looks nice, and is a cornerstone of resource management in valid C++ code. Unfortunately life is not as easy as it is. In all the above examples, we’re using objects that we’ve used for fire-and-forget use cases. We possibly only read from the object, or executed some logic that it exposed. However, there are often scenarios, where it is not that easy. Scenarios, in which the actual identity or ownership of the passed object is important. Situations where we need to perform some operations on such objects and then still return it, with all the state-altering we’ve done. Sometimes even to actually make copies of such objects. That’s when copy&move semantics come into play.
Special object functions a.k.a. copy&move semantics
In this subchapter I’ve originally used trivial example taken from ‘C++ Crash Course’. However, it was reviewed by my C++ coach, and the result is a better one presented below. Here I want to make a point, before we go further. In general, modern C++ relies heavily on RAII and STL classes, and code likethe one here, should be an exception, used only when absolutely necessary. Whenever possible you should use types from theSTL, which tackle the resource management on their own. Here, I’m using array example for the sake of clarity and readability.
Let’s assume we have an object that looks like this:
#include <cstring> #include <iostream> #include <stdexcept> #include <string_view> #include <utility> class SimpleString { std::size_t max_size_; std::size_t length_ = 0; char* buffer_; public: explicit SimpleString(std::size_t max_size): max_size_{max_size}, buffer_(new char[max_size_ + 1]) { buffer_[0] = 0; } ~SimpleString() { delete[] buffer_; } const char* c_str() const { return buffer_; } void append(std::string_view txt) { // validation for length if (txt.size() + length_ > max_size_) throw std::length_error("Can't append: string too long"); std::memcpy(buffer_ + length_, txt.data(), txt.size()); length_ += txt.size(); // make sure to have NULL-termination buffer_[length_] = 0; } };
What we’re most interested in here, is the ownership of the resource. In the constructor we allocate a specified amount of characters in an array, which is dynamically allocated. In the destructor we free this resource. So far RAII is followed to the letter.
Copy semantics
With the above example we have a problem. Assume we’re interested in assigning existing object to the other one. With dynamically allocated resource, it will result in two pointers being created, that point to the specific address in memory (the one occupied by the buffer variable here). Let’s take a look at the below code:
int main() { SimpleString s1 {50}; s1.append("Append line 1"); SimpleString s2 = s1; s2.append("Append line 2"); s1.print("Print1"); s2.print("Print2"); }
What gets printed as a result?
Print1: Append line 1 Append line 2 Print2: Append line 1 Append line 2
You may wonder what just happened? Well, it’s simple if you look at it – by performing instantiation to s2 with already existing s1, we’ve made the copy of the existing object (shallow copy to be exact). The copy in this regard meaning ‘copy of the pointer that points to the first element in the array’. Therefore, all the subsequent changes (appending line), are reflected in the original, dynamically allocated memory. Obviously, we don’t want that. We want our s2 instance to have (at the beginning) values from s1, but later, we don’t want it to influence already existing s1 instance.
In order to achieve that, we need to take control of the copy operation. How can we do that? Simply by defining appropriate functions. Why functions (plural)? Because it’s possible in C++ to make a copy of an object in two ways – during construction and during assignment:
// Behaviour is the same, but underlying mechanism is different in SimpleOperation s2 = s1!!! SimpleString s3 {s1};
The code of our class that should be there looks like this – notice two methods being added:
// SimpleString s3 {s1}; SimpleString(const SimpleString& other) : max_size_{other.max_size_}, length_{other.length_}, buffer_{new char[other.max_size_ + 1]} { std::memcpy(buffer_, other.buffer_, length_ + 1); } // SimpleString s2 = s1; SimpleString& operator=(const SimpleString& other) { if(this == &other) return *this; // To avoid unnecessary freeing the resources of the actual instance const auto new_buffer = new char[other.max_size_ + 1]; // Allocate new dynamic array max_size_ = other.max_size_; length_ = other.length_; std::memcpy(new_buffer, other.buffer_, length_ + 1); delete[] buffer_; buffer_ = new_buffer; // Assign newly allocated memory to new handle return *this; }
If we rerun this code:
int main() { SimpleString s1 {50}; s1.append("Append line 1"); SimpleString s2 = s1; s2.append("Append line 2"); s1.print("Print1"); s2.print("Print2"); }
The output will be completely different:
Print1: Append line 1 Print2: Append line 1 Append line 2
That is what we’re hoping for. Instance s2 contains the value that s1 originally had, but adding some additional line to it, does not cause original s1 to be affected. Unlike in Java 😉
Move semantics
So far, so good. We know that copying objects in C++, which handle resources by themselves can be a dangerous thing, and caution is advised. The same rule applies to the situation, in which the resources handled by user-defined class are heavy (in terms of memory/latency/locking). Our simple example from above could be seen as such, if we just make the buffer large enough (say, 1 million characters). Going with copying may take time, especially if the original object we have (s1 in our examples) won’t be used later. Something like here:
int main() { SimpleString s1 {1500000}; s1.append_line("veeeeery looooooong striiiinnnnng"); SimpleString s2 = s1; // After this copy s1 is not used anymore s2.append_line("even longer one"); s2.print("Print2"); }
Here, s1 actually is not used after copying. It may seem like a huge waste – to copy value of s1, and then leave it waiting there idle. In order to address such issues, a concept of move semantics emerged (starting in C++11). In short – if we don’t care about the variable that is our source, we may actually ‘steal’ its contents, by performing a move operation. However, in order to fully get a grasp of it, we have to tell something about lvalues and rvalues.
Here, I would use a direct quote from ‘C++ Crash Course’:
We’ll consider a very simplified view of value categories. For now, you’ll just need a general understanding of lvalues and rvalues. An lvalue is any value that has a name, and an rvalue is anything that isn’t an lvalue.
To make it easier – here are the usual things that are rvalues – temporary objects, literal constants, function return values (unless they’re lvalues passed as params) and usually results of built-in operators. Lvalues that were parameters to std::move() too (more on it later).
I know that it might sound like explaining the unknown through unknown, however, I think that simple code example (taken from the same book) would explain the concept:
#include <cstdio> void ref_type(int &x) { printf("lvalue reference %d\n", x); } void ref_type(int &&x) { printf("rvalue reference %d\n", x); } int main() { auto x = 1; ref_type(x); ref_type(2); ref_type(x + 2); }
The output is:
lvalue reference 1 rvalue reference 2 rvalue reference 3
How are lvalues and rvalues involved in move semantics? Well, actually rvalues are necessary to perform move semantics in general. If we’re interested in adding move semantics to our classes, we have yet again define two methods that are responsible for that. The original thing here is, that they accept rvalue as a parameter.
// Pay attention to both RVALUE as param and 'noexcept' SimpleString(SimpleString&& other) noexcept : max_size_{std::exchange(other.max_size_, 0)}, length_{std::exchange(other.length_, 0)}, buffer_{std::exchange(other.buffer_, nullptr)} { } SimpleString& operator=(SimpleString&& other) noexcept { delete[] buffer_; // we clear current heap-allocation to avoid leak max_size_ = std::exchange(other.max_size_, 0); length_ = std::exchange(other.length_, 0); buffer_ = std::exchange(other.buffer_, nullptr); return *this; }
We have a couple of changes here that we need to look at.
- noexcept – in general we don’t expect these methods to throw any kind of exceptions. We cannibalize the source object, and all the operations are either simple copies of values or reassignment of memory addresses.
- no const – pay attention to that! As we’re ’emptying’ the source object, we must be able to actually change its state. Therefore we cannot use const for the parameter.
- emptying source object – already mentioned this one. In general, we leave the source object in (as standard says) “valid but unspecified state”. There’s no problem with reusing existing reference to assign a new object to it, however the reference itself at this time is ’empty’.
The last point above can be a source of the problems. The programmer must always remember to clean up the source object, in order to avoid nasty runtime errors (like double-free ones). Therefore, when possible, try to reuse existing move constructors/operators, or use
std::exchange
function, that performs move operation, but also nulls the source object.
Ok, but how I get rvalue?
That’s the valid point. In general, if we create a new object, and at the same time we pass it to the constructor (or assign it) we should be fine. However, the examples above were showing assigning already existing object/reference (so – lvalue). To help us with move semantics C++ introduced a function in the STD called move. Its purpose is to cast any lvalue to rvalue, and therefore to use existing references in the move semantics. We can see its usage with the code presented above, with one small change:
#include <cstdio> #include <utility> void ref_type(int &x) { printf("lvalue reference %d\n", x); } void ref_type(int &&x) { printf("rvalue reference %d\n", x); } int main() { auto x = 1; ref_type(std::move(x)); ref_type(2); ref_type(x + 2); }
Rule of 0/3/5 a.k.a. should I even use this?
Above code samples showed, what is possible in C++ using copy&move semantics. You should also remember, that I’ve stated quite avowedly, that example provided here, is in a way ‘bad’. Why? Because in general, unless we’re creating wrappers for some resources ourselves (not just memory – locks/UI elements/connections/file handles) we should let the compiler and STL do the work. In other words – our example should be just rewritten using std::string instance, which handles the memory/capacity on its own. RAII is a powerful feature, and should be used whenever possible. Same applies to resource-wrapping classes. STL is full of them, and every presentation/blog post I’ve read about copy&move semantics was very clear about it. If you feel the urge to write copy&move semantics methods or a custom destructor, please, take a moment and check if you really have to.
This rule is known as the rule of 0. It is well-explained in the CPP Guidelines – unless you really have to, you should create code that complies with this rule. Period.
A follow-up rule (that in C++ guidelines is next one) is a rule of five. In general, it says, that whether you define, =default or =delete one of the copy/move/destructor methods you should do the same for all others. Their semantics is closely related, and they should be treated as a concise unit of change.
Prior to C++11 a rule of three was in use, which said the same as the rule of five, although due to absence of move semantics back then, it was restricted only to copying methods.
Summary
That was quite a lot of material, and there’s way more in the sources list below. Concepts described here are foundation for writing robust and correct C++ code, so they shouldn’t be thread lightly. Of course, I’m a beginner here, so I need a lot of practice too in that area 😉
Below you can find a simple code snippet, that will show the usage of our SimpleString class in the client code.
struct MyObject { int i = 5; SimpleString s{256}; }; void foo(MyObject obj) { obj.s.append("ghi"); std::cout << obj.s.c_str() << '\n'; } int main() { try { SimpleString s(6); s.append("123"); s.append("456"); // s.append("7"); // throws exception std::cout << "s: " << s.c_str() << "\n"; SimpleString s1(50); s1.append("abc"); SimpleString s2 = s1; // copy-construction s2.append("def"); std::cout << "s1: " << s1.c_str() << "\n"; std::cout << "s2: " << s2.c_str() << "\n"; SimpleString s3(s2); // also copy-construction std::cout << "s3: " << s3.c_str() << "\n"; s3 = s1; // copy assignment std::cout << "s3: " << s3.c_str() << "\n"; auto s4 = SimpleString(1500000); s4.append("veeeeery looooooong striiiinnnnng"); SimpleString s5 = std::move(s4); // move construction s5.append(" even longer one"); std::cout << "s5: " << s5.c_str() << "\n"; // std::cout << "s4: " << s4.c_str() << "\n"; // not legal to use s4 before assigning a new state s4 = std::move(s5); // move asignment std::cout << "s4: " << s4.c_str() << "\n"; // now we cannot use s5 MyObject obj1; obj1.s.append("abc"); MyObject obj2 = obj1; obj2.s.append("def"); foo(obj2); std::cout << obj1.s.c_str() << '\n'; std::cout << obj2.s.c_str() << '\n'; } catch(const std::exception& ex) { std::cout << "Unhandled exception: " << ex.what() << '\n'; } }
SOURCES:
- Josh Lospinoso ‘C++ Crash Course’ book
- Back to basics – Move semantics – CPPCon 2020
- Hidden secrets of – move semantics – CPPCon 2020
- Klaus Iglberger Back to Basics: Move Semantics part one and part two from CppCon 2019
- CPP Con back to basics RAII and the rule of zero
- CPP Con back to basics smart pointers
- Notes on copy&move semantics
- Youtube playlist created for Java programmers learning C++
- Andrzej Krzemiński post about value semantics
- List of articles in the subject on ModernesCPP
One Comment
Leave a Reply
You must be logged in to post a comment.
GC Theory – Reference counting explained – Bare.Metal.Dev
[…] I’ve mentioned that one of the biggest problem with ref counting is the storage used to hold the counter. In the worst case scenario, it should be able to hold as much as the count of all the objects on the stack and heap. Obviously, such situation is impossible to come by in real life, but still the question arises – how large should it be? The studies suggested, that most of the objects are unique, and therefore they don’t need that much of counting possibilities (especially in the functional languages). It also enables to perform runtime optimizations such as move semantics (I’ve written an article about it in C++). […]