C++ for Java programmers

Wanasit Tanakitrungruang
9 min readSep 23, 2022

--

In the past few months, I have switched my primary programming language from Java to C++ for an SWE role in the new company. Before that, I had been programming in Java and other high-level languages (Javascript and Python) for around ten years. Learning C++ this late in my career has been eye-opening.

In this article, I want to share C++ concepts or topics that surprised and confused me when switching from Java. I believe knowing these topics is useful even if you do not plan to switch to C++. For people who are looking to read or learn C++, these are things that you may have misunderstood and should pay attention to:

C++ is very different from C

Whether you write Java or Javascript (or other languages), you probably have heard about the “JavaScript is not Java” joke. It is safe to assume that every programmer knows Java and JavaScript are different languages.

However, I think many programmers probably do not know C and C++ are also very different languages. Or, many of us often mistakenly group them as “C/C++” programming.

Before working professionally, I studied C programming in college. I used the language on several projects related to low-level and embedded systems. I have a working knowledge of memory management without Garbage Collection. However, I have found my C experience does not apply to C++ as well as I thought.

Technically, C++ is a superset of C. Indeed, any program written in C would compile and run in C++. The compiler and build tools are also areas where C++ and C are similar (e.g. both have separated headers, .h, and implementation, .cor .cc, files, the implementation needs to be compiled and linked, etc).

In practice, however, no one would write C code in a C++ program. Most C++ programmers do not treat C++ as C with extension (or C with Classes). They simply use language features that are unique to C++. So, even when solving the same problem, the programs written in C and C++ are often very different. So far, I have never seen any C or C-style code in our company's large C++ codebase.

Objects can be created without `new`

In Java, the only way to create the object is to call the constructor with new keyword.

DateTime date = new DateTime("2012-08-16T07:22:05Z");

This expression allocates memory for DateTime’s instance (in heap), calls the DateTime constructor (with the String), and assigns to the variable date a reference or pointer to the created DateTime object.

The most similar expression in C++ (assuming there is a similar DataTime class) would be:

DateTime *date = new DateTime("2012-08-16T07:22:05Z");...delete date; // sometime later to return the memory

The new keyword in C++ also allocates the instance in the heap, initializes the object with the constructor, and returns the address to the instance.

(Note that I believe the Java’s Objects variables are equivalent to C++’s pointers, not references. More on that later)

However, in practice, C++ programs often do not create objects on the heap using the new keywords. Overusing heap allocation is even considered a bad practice. Instead, the most common object creation is creating the objects on the stack:

DateTime a1 = DateTime();
DateTime a2 = DateTime("2012-08-16T07:22:05Z");
DateTime b1; // Similar to `DateTime b1 = DateTime();`
DateTime b2 = a1; // Similar to `DateTime b2 = DateTime(a1);`
DateTime b3 = "2012-08-16T07:22:05Z";
// Similar to `DateTime b3 = DateTime("2012-08-16T07:22:05Z");`

In all of the above, the objects are allocated in the function’s stack memory (similar to how primitive local variables are allocated in Java). The local objects or variables will all be cleaned up when the function finishes.

The a1 and a2 initialization syntax is easily recognizable as calling the constructor (but without new). However, how b1, b2, and b3 are created and initialized may confuse non-C++ programmers.

  • The b1 is created and initialized in the code above with the default constructor. A lot of C++ features rely on being able to create objects implicitly with the default constructor (e.g. when creating an array DateTime array[3] or using the object as a key in STL).
  • The b2 is created by the copy constructor. This copy constructor is invoked when we try to assign an object by value (more on that later).
  • The b3 is created by a conversion constructor. Any single-parameter constructor can be a conversion constructor (unless it's specified as explicit). Overuse of the conversion can be confusing and often considered a bad practice. However, they are still used in practice. The most common example is converting literal char[] to std::string (e.g. std::string word = "Hello";)

Those are just a few examples of how objects are created and what constructors are called (explicitly or implicitly). Understanding different ways objects are created without seeing the new keyword and the explicit constructor call is an important challenge ex-Java programmers would have to adapt.

Assigning by Value vs Reference

In Java (and probably most languages), we learn that, when we assign a variable to another, its value is copied into the target variable.

For primitive types, after assigning the value, there will be no further link between the two. Later changing the target variable should not affect the original variable. For example:

int a = 12;
int b = a; // now, a = 12
b = 13; // still a = 12
// Similar thing when calling a function
void setInt(int y) { y = 123; }
int x = 24;
setInt(x); // After the function, x is still 24

What about Object-type? How can there be a function that modifies the input object e.g. Collections.sort(x) ?

The same rule is still applied, but because an Object variable is a pointer (to the heap memory allocated by new explained above), its pointed Object’s address is copied into another variable. When we modify the Object in that, the same updated value can be seen from both variables. For example:

List<String> x = new ArrayList<>(); // `x` points to a new list
List<String> y = x;
// `x`'s value (Object address) is copied to `y`
y.append("Hello"); // Update the object that is pointed by `y`
y.append("World");
// Now, both `x` and `y` point to ["Hello", "World"]

In Java, this assigning/passing by value is the only assignment type allowed. C++, however, supports both assigning/passing by value and reference.

By Reference

C++ has “reference” types (e.g. int& reads "int-reference"). You can assign a variable to a reference, or you can pass a variable to functions that accept reference (aka "pass-by-reference").

As the name suggests, assigning/passing-by-reference does not copy the variable content but passes the actual variable into the target directly as a reference.

std::vector<std::string> x = {"Hello"};    
std::vector<std::string>& y = x; // `y` is `x`
y.push_back("World");
// Now, both `x` and `y` is {"Hello", "World"}
y = {"Goodbye"}
// Now, both `x` and `y` is {"Goodbye"}

At first, you may think passing a reference is similar to passing an object pointer, but they are different.

In the above example, the reference y does not store the same value or address to x. It simply is x (aliasing). Changing y is equivalent to changing x . This includes reassigning a whole new vector to y.

Working with references needs some adjustment, but most programmers often get familiar with them relatively quickly and naturally. For example, passing constant references (const Object&) is often the recommended approach to passing an object as read-only input. Passing modifiable reference is sometimes used for passing secondary output. They benefit from the fact that reference cannot be NULL (unlike pointers).

By Value

While working with references requires slight adjustment, the typical assigning by value in C++, however, may confuse or surprise you if you come from other languages. It requires deep understanding and caution to use it correctly and efficiently.

Similar to Java, when we assign a variable by value, its value is copied into the target variable. But, unlike other languages mentioned previously, assigning an Object by value in C++ often means copying or serializing the whole Object (not the pointer’s address copy), and it sometimes has a significant efficiency impact.

vector<std::string> x = {"hello", "world", ..., } 
vector<std::string> y = x;
// Similar to `vector<string> y = vector<string>(x);`
y.push_back("goodbye"); // Update `y`// Now:
// `x` is {"hello", "world", ..., }
// `y` is {"hello", "world", ..., "goodbye"}
// and they are two separate copies (shared nothing)

The second line in the above example triggers copy assignment operator and copy constructor (which recursively calls copy constructor for each element). If x has N elements, that line alone would take O(N) time and O(N) additional space. This is often not what you want.

There is another example, I have seen the above example sometimes in coding snippets:

bool BinarySeach(const vector<string> list, const string value) {
// Clone/copy the whole list/vector into the function
// This is often NOT what you want
...
}
bool BinarySeach(const vector<string>& list, const string& value) {
// The correct approach is passing the readonly reference - &
...
}

Many modern C++ IDE or static-analyzer should give a warning on the first function. But, if you declare the function header as the first function, simply calling the function makes a full memory copy of the list. It takes at least O(N) time and additional space, no matter what search algorithm is used inside the function.

Polymorphism at Run-time vs Compile-time

Polymorphism is the provision of a single interface to entities of different types
From Wikipedia, (which is actually from the C++ Glossary)

Polymorphism is an important concept to master in programming languages. It allows us to use different classes or types the same way based on their shared interface or commonality. It also allows us to design a single program that works well with new types or future changes.

In OOP languages, Polymorphism often focuses on Inheritance or Sub-classing. For example, in Java, we often write a program that operates on an interface or abstract class. We extend the program by providing different implementations.

Polymorphism by sub-classing is, however, not the only Polymorphism possible. In C++ terminology, Polymorphism by sub-classing (or virtual functions) is called Runtime Polymorphism (or Dynamic Polymorphism), as the objects’ behavior is decided at runtime. The different and more common type is Compile-time Polymorphism (or Static Polymorphism).

The easiest-to-understand example of compile-time polymorphism is Function Overloading. You can call the same function (name) with different input types and compiler links to the appropriate implementation for your program.

C++’s Templates Programming is another compile-time polymorphism but even more sophisticated.

The best way to explain how C++’s template and compile-time polymorphism work in practice is by comparing Java’s Collection Library (injava.util.*) with C++’s Standard Template Library (STL).

Java Collection

Below is an example of how to build an inverted index in Java with the standard collection library.

Map<String, Collection<Integer>> index = new HashMap<>();for (int i=0; i<lines.size(); i++) {
for (String word: lines.get(i).split()) {
index.pushIfAbsent(word, new HashSet<>());
index.get(word).add(i);
}
}
  • We declare the index as a map of strings to collections of integers. The index type is an interface, thus, we can assign it a new HashMap. Alternatively, we could assign it a TreeMap or any other type of map.
  • Similarly, each index’s value entry or the posting list is defined as a Collection interface. In the example, each posting list is HashSet but we can replace them with ArrayList (e.g. if we know that each line has distinct words).

Note how Map and Collection interfaces can work together regardless of the allow different sub-classes.

C++ STL

unorder_map<string, unordered_set<int>> index;for (int i=0; i<lines.size(); i++) {
for (string word: lines[i].split()) {
index[word].insert(i);
}
}

In C++ STL, unordered_map and map (which is a tree map) are two different classes/types. They don't have a common parent or shared interface. Similarly, unordered_set is not set or vector. Those classes have no common abstraction.

So, unlike Java, we do not declare the index as unordered_map<string, Collection> where the Collection is an interface or abstract class, and then assign an implementation at the runtime. For the C++ template to work, the compiler needs to know the actual type to generate the map code that works correctly.

On the other hand, however, because the compiler knows what exactly the index’s entry value is unordered_set, the compiler knows how to create a new value of that type (via default constructor as explained above) for the templated map when the missing entry is accessed. For example, unlike Java, manual initialization with pushIfAbsent is not necessary.

If you have read until this part, you should agree that C++ is very different from most languages and it is not an easy language to master. These days, I still continue learning and discovering new things that challenge my understanding of the language.

This article is not a comprehensive list of C++’s important concepts. They are simply my top picks for things I wish I knew earlier. I believe knowing about these topics is useful even if you do not use C++.

--

--

Wanasit Tanakitrungruang

A software engineer interested in Search, and ML problems. Currently working as a Software Engineer in @Google Tokyo.