What is Modern C++?

Since 1979, when Bjarne Stroustrup, a Danish computer scientist, began working on “C with Classes”, the predecessor to C++, the C++ programming language has expanded significantly, and modern C++ now has object-oriented, generic, and functional features in addition to facilities for low-level memory manipulation. The C++11 standard brought many useful new features. Modern C++ code is clean, safe, and fast – just as clean and safe as code written in any other modern mainstream language, and with C++’s traditional to-the-metal performance as strong as ever.

18 powerful, proven benefits of modern C++ …

The C++ Standard

C++ is standardized by ISO (The International Standards Organization) in collaboration with national standards organizations, such as ANSI (The American National Standards Institute), BSI (The British Standards Institute) and DIN (The German national standards organization). It is an internationally agreed to document that specifies the behavior of our compilers and programs.

The value of the standardization of C++ should not be underestimated. C++ became a better language through standardization and acquired a standard library of surprising expressive power. Formal standardization is one of the few defenses that a user has against the interests of compiler suppliers, who may try to lock in their users.

The C++ Standard Library provides generic containers and functions to use and manipulate these containers, function objects, generic strings and streams (including interactive and file I/O), support for some language features, and functions for everyday tasks such as finding the square Square root of a number.

A noteworthy feature of the C++ Standard Library is that it not only specifies the syntax and semantics of generic algorithms, but also places requirements on their performance.These performance requirements often correspond to a well-known algorithm, which is expected but not required to be used.

const correctness

Using the keyword const prevents const objects from getting mutated.

One of the most important tools to clean code. An object declared const or accessed via a const reference or const pointer cannot be modified. The compiler enforces this. Just as important it forces us to initialize the value.

For example, if you wanted to create a function f() that accepted a std::string, plus you want to promise callers not to change the caller’s std::string that gets passed to f(), you can have f() receive its std::string parameter…

  • void f1(const std::string& s); // Pass by reference-to-const
  • void f2(const std::string* sptr); // Pass by pointer-to-const
  • void f3(std::string s); // Pass by value

In the pass by reference-to-const and pass by pointer-to-const cases, any attempts to change the caller’s std::string within the f() functions would be flagged by the compiler as an error at compile-time. This check is done entirely at compile-time: there is no run-time space or speed cost for the const. In the pass by value case (f3()), the called function gets a copy of the caller’s std::string. This means that f3() can change its local copy, but the copy is destroyed when f3() returns. In particular f3() cannot change the caller’s std::string object.

In C++, you can use the const keyword instead of the #define pre-processor directive to define constant values. Values defined with const are subject to type checking, and can be used in place of constant expressions.

constexpr

The constexpr specifier declares that it is possible to evaluate the value of the function or variable at compile time.

The keyword constexpr was introduced in C++11 and improved in C++14. It means constant expression. Like const, it can be applied to variables. A compiler error is raised when any code attempts to modify the value. Unlike const, constexpr can also be applied to functions and class constructors. constexpr indicates that the value, or return value, is constant and, where possible, is computed at compile time.

A constexpr integral value can be used wherever a const integer is required, such as in template arguments and array declarations. And when a value is computed at compile time instead of run time, it helps your program run faster and use less memory.

Object lifetime and resource management

Constructor / destructor pairs (RAII) combined with scoped values give us determinism that removes the need for things like finally.

Modern C++ avoids using heap memory as much as possible by declaring objects on the stack. When a resource is too large for the stack, then it should be owned by an object. As the object gets initialized, it acquires the resource it owns. The object is then responsible for releasing the resource in its destructor. The owning object itself is declared on the stack. The principle that objects own resources is also known as “resource acquisition is initialization,” or RAII.

In RAII, holding a resource is a class invariant and is tied to object lifetime: resource acquisition (or allocation) is done during object creation (specifically initialization), by the constructor, while resource deallocation (release) is done during object destruction (specifically finalization), by the destructor. In other words, resource acquisition must succeed for initialization to succeed. The resource is guaranteed to be held between when initialization finishes and finalization starts and to be held only when the object is alive. Thus if there are no object leaks, there are no resource leaks.

When a resource-owning stack object goes out of scope, its destructor is automatically invoked. In this way, garbage collection in C++ is closely related to object lifetime, and is deterministic. A resource is always released at a known point in the program, which you can control. Only deterministic destructors like those in C++ can handle memory and non-memory resources equally.

Templates

C++ templates enable generic programming. C++ supports function, class, alias, and variable templates. Templates may be parameterized by types, compile-time constants, and other templates. Templates are implemented by instantiation at compile-time. To instantiate a template, compilers substitute specific arguments for a template’s parameters to generate a concrete function or class instance.

Some substitutions are not possible; these are eliminated by an overload resolution policy described by the phrase “Substitution failure is not an error” (SFINAE). Which refers to a situation in C++ where an invalid substitution of template parameters is not in itself an error.

Templates are a powerful tool that can be used for generic programming, template meta-programming, and code optimization, but this power has a cost. Template use may increase code size, because each template instantiation produces a copy of the template code: one for each set of template arguments, however, this is the same or smaller amount of code that would be generated if the code was written by hand.

The ultimate in the DRY (Do not Repeat Yourself) principle. You can write a template that has types and values filled in at compile-time. Template types are generated by the compiler at compile time. No need for any kind of type-erasure (like in Java generics). Highly efficient runtime code is possible, which is as good as (or better than) hand writing the various options.

The STL (Standard Template Library)

The C++ Standard Template Library provides four components, namely:

  1. algorithms
  2. containers
  3. functions
  4. iterators

The STL provides a set of common classes for C++, such as containers and associative arrays, that can be used with any built-in type and with any user-defined type that supports some elementary operations (such as copying and assignment). STL algorithms are independent of containers, which significantly reduces the complexity of the library.

The STL achieves its results through the use of templates. This approach provides compile-time polymorphism that is often more efficient than traditional run-time polymorphism. Modern C++ compilers are tuned to minimize abstraction penalties arising from heavy use of the STL.

std::array

std::array A fixed-size stack-based container. Having the size part of the type information gives more optimization opportunities. std::array is a container that encapsulates fixed size arrays. This container is an aggregate type with the same semantics as a struct holding a C-style array T[N] as its only non-static data member. Unlike a C-style array, it doesn’t decay to T* automatically. As an aggregate type, it can be initialized with aggregate-initialization given at most N initializers that are convertible to T: std::array<int, 3> a = {1,2,3};.

The struct combines the performance and accessibility of a C-style array with the benefits of a standard container, such as knowing its own size, supporting assignment, random access iterators, etc.

Uniform initialization

In modern C++, you can use brace initialization for any type. This form of initialization is especially convenient when initializing arrays, vectors, or other containers. List Initialization initializes an object from braced-init-list{}

int a[] = { 1, 2 }; // array initializer
struct S { int x, string s };
S s = { 1, "Helios" }; // struct initializer
complex<double> z = { 0, pi }; // use constructor
vector<double> v = { 0.0, 1.1, 2.2, 3.3 }; // use list constructor

From Bjarne Stroustrup’s “The C++ Programming Language 4th Edition”:

List initialization does not allow narrowing. That is:

  • An integer cannot be converted to another integer that cannot hold its value. For example, char to int is allowed, but not int to char.
  • A floating-point value cannot be converted to another floating-point type that cannot hold its value. For example, float to double is allowed, but not double to float.
  • A floating-point value cannot be converted to an integer type.
  • An integer value cannot be converted to a floating-point type.

The only situation where = is preferred over {} is when using auto keyword to get the type determined by the initializer.

Variadic Templates

Variadic templates are templates that take a variable number of arguments. This simplifies code needing to match a variable number of parameters. Because it allows template definitions to take an arbitrary number of arguments of any type.

void tprintf(const char* format) // base function
{
    std::cout << format;
}
 
template<typename T, typename... Targs>
void tprintf(const char* format, T value, Targs... Fargs) // recursive variadic function
{
    for ( ; *format != '\0'; format++ ) {
        if ( *format == '%' ) {
           std::cout << value;
           tprintf(format+1, Fargs...); // recursive call
           return;
        }
        std::cout << *format;
    }
}
 
int main()
{
    tprintf("% world% %\n","Hello",'!',123);
    return 0;
}

The above example defines a function similar to std::printf, that replace each occurrence of the character % in the format string with a value.

Automatic deduction of value types with auto

The increasingly widespread application of type deduction frees you from having to spell out types that are obvious or redundant. It makes C++ software more adaptable, because changing a type at one point in the source code automatically propagates through type deduction to other locations.

Return type deduction for functions

Since C++14, when using auto with functions, the compiler can deduce return types for any function, no matter how complex. The only condition is that each return statement must have the exact same type. The rules then are the same as for auto variables.

C++11 permited return types for single-statement lambdas to be deduced, and C++14 extends this to both all lambdas and all functions, including those with multiple statements. Compilers will deduce the function’s return type from the function’s implementation.

Note: Using auto in a function return type or a lambda parameter implies template type deduction, not auto type deduction.

Lambdas

Lambdas enable you to create unnamed function objects which may or may not have captures. Lambdas are a convenient way of defining an anonymous function object (a closure) right at the location where it is invoked or passed as an argument to a function. Typically lambdas are used to encapsulate a few lines of code that are passed to algorithms or asynchronous methods.

The main use of lambdas is for specifying code to be passed as arguments. Lambdas allow that to be done ‘‘inline’’ without having to name a function (or function object) and use it elsewhere. Some lambdas require no access to their local environment. Such lambdas are defined with the empty lambda introducer []. For example:

    void algo(vector<int>& v)
   {
    sort(v.begin(),v.end(),[](int x, int y) { return abs(x)<abs(y);}); // sort absolute values
    }

Range-based for loops

Executes a for loop over a range. Used as a more readable equivalent to the traditional for loop operating over a range of values, such as all elements in a container. Works with anything that you can iterate through, that has begin() and end() members functions, including C-style arrays and braced initializer lists.

Range based for loops often use the auto specifier for automatic type deduction.

std::vector<int> v {0, 1, 2, 3, 4, 5};
 
    for (const int& i : v) // access by const reference
        std::cout << i << ' ';
    std::cout << '\n';
 
    for (auto i : v) // access by value, the type of i is int
        std::cout << i << ' ';
    std::cout << '\n';
 
    for (auto&& i : v) // access by forwarding reference, the type of i is int&
        std::cout << i << ' ';
    std::cout << '\n';
 
    const auto& cv = v;
 
    for (auto&& i : cv) // access by f-d reference, the type of i is const int&
        std::cout << i << ' ';
    std::cout << '\n';
 
    for (int n : {0, 1, 2, 3, 4, 5}) // the initializer may be a braced-init-list
        std::cout << n << ' ';
    std::cout << '\n';
 
    int a[] {0, 1, 2, 3, 4, 5};
    for (int n : a) // the initializer may be an array
        std::cout << n << ' ';
    std::cout << '\n';
 
    for ([[maybe_unused]] int n : a)  
        std::cout << 1 << ' '; // the loop variable need not be used
    std::cout << '\n';
     
        for (auto n = v.size(); auto i : v) // the init-statement (C++20)
            std::cout << --n + i << ' ';
        std::cout << '\n';

Structured Bindings

Binds the specified names to sub-objects or elements of the initializer.

Like a reference, a structured binding is an alias to an existing object. Unlike a reference, a structured binding does not have to be of a reference type. Used to decompose a structure or array into a set of identifiers. You must use auto, and the number of elements must match. There’s no way to skip an element.

Using structured bindings in C++17, you can, for example, bind tuple members directly to named variables without having to use std::get, or declaring the variables first. This technique will allow you to obtain references to tuple members, something that isn’t possible using std::tie. Here, you’ll get references to the tuple members, and when you change the value of one of them, the value in the tuple changes:

auto t0 = std::make_tuple(10, 21);
auto& [first, second] = t0;
second += 2;
std::cout << "value is now " << second << std::endl;

Tuples are an obvious use case, but structured bindings can also be used with classes, structs and arrays:

struct Person {
	std::string name;
	uint32_t age;
	std::string city;
};
 
Person p1{"Bob Jones", 32, "Boston"};
auto [name, age, city] = p1;
std::cout << name << " is "
<< age << " years old and lives in " << city << std::endl;

std::string_view

std::string_view is a non-owning “view” of a string like structure. C++17 introduced another way of using strings, std::string_view, which lives in the <string_view> header.

Unlike std::string, which keeps its own copy of the string, std::string_view provides a view of a string that is defined elsewhere. The output is the same, but no more copies of the string are created. The string is stored in the binary and is not allocated at run-time. The main purpose of std::string_view is to avoid copying data which is already owned and of which only a non-mutating view is required.

In cases where string are too large to benefit from Small String Optimization (SSO), using std::string_view is much faster than std::string, and has it many of the same member functions.

Smart pointers

In modern C++ programming, the Standard Library includes smart pointers, which are used to help ensure that programs are free of memory and resource leaks and are exception-safe. They are crucial to the RAII (Resource Acquisition Is Initialization) programming idiom.

C++ Standard Library smart pointers:

  • unique_ptr
  • shared_ptr
  • weak_ptr

C++11 introduced std::unique_ptr, defined in the header <memory>.

A unique_ptr is a container for a raw pointer, which the unique_ptr is said to own. A unique_ptr explicitly prevents copying of its contained pointer (as would happen with normal assignment), but the std::move function can be used to transfer ownership of the contained pointer to another unique_ptr. A unique_ptr cannot be copied because its copy constructor and assignment operators are explicitly deleted.

C++11 also introduced std::make_shared (std::make_unique was introduced in C++14) to safely allocate dynamic memory in the RAII paradigm.

A shared_ptr maintains reference counting ownership of its contained pointer in cooperation with all copies of the shared_ptr. An object referenced by the contained raw pointer will be destroyed when and only when all copies of the shared_ptr have been destroyed.

A weak_ptr is created as a copy of a shared_ptr. The existence or destruction of weak_ptrcopies of a shared_ptr have no effect on the shared_ptr or its other copies. After all copies of a shared_ptr have been destroyed, all weak_ptr copies become empty.

Move semantics

Modern C++ provides move semantics, which make it possible to eliminate unnecessary memory copies. In earlier versions of C++, copies were unavoidable in certain situations. A move operation transfers ownership of a resource from one object to the next without making a copy. Some classes own resources such as heap memory and file handles.

Move constructors typically “steal” the resources held by the argument (e.g. pointers to dynamically-allocated objects, file descriptors, TCP sockets, I/O streams, running threads, etc.) rather than make copies of them, and leave the argument in some valid but otherwise indeterminate state. For example, moving from a std::string or from a std::vector may result in the argument being left empty. However, this behavior should not be relied upon. For some types, such as std::unique_ptr, the moved-from state is fully specified.

Parallel Algorithms

With C++17, concurrency in C++ has been greatly improved. C++11 and C++14 only provided the basic building blocks for concurrency. The performance that can be achieved by using a parallelized version of an algorithm can be great and especially useful in data science applications.

Parallel algorithms of the Standard Template Library (STL). Since C++17, most of the STL algorithms are available in a parallel implementation. This makes it possible for you to invoke an algorithm with an execution policy. This policy specifies whether the algorithm runs sequentially (std::execution::seq), in parallel (std::execution::par),or in parallel with additional vectorization (std::execution::par_unseq).

The usage of the execution policy std::execution::par or std::execution::par_unseq allows the algorithm to run parallel or parallel and vectorized. This is a permission and not a requirement.

std::vector<int>v { 2,3,4,8,6,7,1,9 };
std::sort(v.begin(), v.end());
std::sort(std::execution::seq, v.begin(), v.end());
std::sort(std::execution::par, v.begin(), v.end());
std::sort(std::execution::par_unseq, v.begin(), v.end());

Parallelization can deliver huge wins but choosing where to apply it is important. Parallel algorithms depend on available hardware parallelism, so ensure you test on hardware whose performance you care about. You don’t need a lot of cores to show wins, and many parallel algorithms are divide and conquer problems that won’t show perfect scaling with thread count anyway, but more is still better.

Learn more about the benefits of programming your applications with modern C++

Related services: C++ Software Development