So, you came across the Modern C++ & overwhelmed by its features in terms of performance, convenience & code expressiveness. But in a dilemma that how you can spot where you can enforce Modern C++ features in your day to day coding job. No worries, here we will see 21 new features of Modern C++ you can use in your project.
/!\: Originally published @ www.vishalchovatiya.com.
C++ community releasing new standards more frequently than iPhone releases. Due to this, C++ now becomes like an elephant and it is impossible to eat the whole elephant in one go. That is why I have written this post to kick start your Modern C++ journey. Here my intended audience is peeps who are moving from older(i.e. 98/03) C++ to Modern(i.e. 2011 onwards) C++.
I have chosen some of the Modern C++ features & explained it with the minimalistic example to make you aware that how you can spot the places where you can employ new features.
1. Digit separators
int no = 1'000'000; // separate units like, thousand, lac, million, etc. long addr = 0xA000'EFFF; // separate 32 bit address uint32_t binary = 0b0001'0010'0111'1111; // now, explanation is not needed i guess
- Earlier you have to count digits or zeros, but now not anymore from C++14.
- This will be useful while counting address in word, half-word or digit boundary or let say you have a credit card or social security number.
- By grouping digits, your code would become more expressive.
2. Type aliases
template <typename T> using dyn_arr = std::vector<T>; dyn_arr<int> nums; // equivalent to std::vector<int> using func_ptr = int (*)(int);
- Semantically similar to using a
typedef
, however, type aliases are easier to read and are compatible with templates types also. Thanks to C++11.
3. User-defined literals
using ull = unsigned long long; constexpr ull operator"" _KB(ull no) { return no * 1024; } constexpr ull operator"" _MB(ull no) { return no * (1024_KB); } cout<<1_KB<<endl; cout<<5_MB<<endl;
- Most of the times you have to deal with real-world jargons like KB, MB, km, cm, rupees, dollars, euros, etc. rather defining functions which do the unit conversion on run-time, you can now treat it as user-defined literals as you do with other primitive types.
- Very convenient for units & measurement.
- Adding constexpr will serve zero cost run-time performance impact which we will see later in this article & I have written a more detailed article on it here.
4. Uniform initialization & Non-static member initialization
Earlier, you have to initialize data members with its default values in the constructor or in the member initialization list. But from C++11, it’s possible to give normal class member variables (those that don’t use the static
keyword) a default initialization value directly as shown below:
class demo { private: uint32_t m_var_1 = 0; bool m_var_2 = false; string m_var_3 = ""; float m_var_4 = 0.0; public: demo(uint32_t var_1, bool var_2, string var_3, float var_4) : m_var_1(var_1), m_var_2(var_2), m_var_3(var_3), m_var_4(var_4) {} }; demo obj{123, true, "lol", 1.1};
- This is more useful when there are multiple sub-objects defined as data members as follows:
class computer { private: cpu_t m_cpu{2, 3.2_GHz}; ram_t m_ram{4_GB, RAM::TYPE::DDR4}; hard_disk_t m_ssd{1_TB, HDD::TYPE::SSD}; public: // ... };
- In this case, you do not need to initialize it in initializer list, rather you can directly give default initialization at the time of declaration.
class X { const static int m_var = 0; }; // int X::m_var = 0; // not needed for constant static data members
- You can also provide initialization at the time of declaration if members are
const
&static
as above.
5. std::initializer_list
std::pair<int, int> p = {1, 2}; std::tuple<int, int> t = {1, 2}; std::vector<int> v = {1, 2, 3, 4, 5}; std::set<int> s = {1, 2, 3, 4, 5}; std::list<int> l = {1, 2, 3, 4, 5}; std::deque<int> d = {1, 2, 3, 4, 5}; std::array<int, 5> a = {1, 2, 3, 4, 5}; // Wont work for adapters // std::stack<int> s = {1, 2, 3, 4, 5}; // std::queue<int> q = {1, 2, 3, 4, 5}; // std::priority_queue<int> pq = {1, 2, 3, 4, 5};
- Assign values to containers directly by initializer list as do with C-style arrays.
- This is also true for nested containers. Thanks to C++11.
6. auto & decltype
auto a = 3.14; // double auto b = 1; // int auto& c = b; // int& auto g = new auto(123); // int* auto x; // error -- `x` requires initializer
-
auto
-typed variables are deduced by the compiler according to the type of their initializer. - Extremely useful for readability, especially for complicated types:
// std::vector<int>::const_iterator cit = v.cbegin(); auto cit = v.cbegin(); // alternatively // std::shared_ptr<vector<uint32_t>> demo_ptr(new vector<uint32_t>(0); auto demo_ptr = make_shared<vector<uint32_t>>(0); // alternatively
- Functions can also deduce the return type using
auto
. In C++11, a return type must be specified either explicitly, or usingdecltype
like:
template <typename X, typename Y> auto add(X x, Y y) -> decltype(x + y) { return x + y; } add(1, 2); // == 3 add(1, 2.0); // == 3.0 add(1.5, 1.5); // == 3.0
- Defining return type as above called trailing return type i.e.
-> return-type
.
7. Range-based for-loops
- Syntactic sugar for iterating over a container's elements.
std::array<int, 5> a {1, 2, 3, 4, 5}; for (int& x : a) x *= 2; // a == { 2, 4, 6, 8, 10 }
- Note the difference when using
int
as opposed toint&
:
std::array<int, 5> a {1, 2, 3, 4, 5}; for (int x : a) x *= 2; // a == { 1, 2, 3, 4, 5 }
8. Smart pointers
- C++11 introduces new smart(er) pointers:
std::unique_ptr
,std::shared_ptr
,std::weak_ptr
. - And
std::auto_ptr
now become deprecated and then eventually removed in C++17.
std::unique_ptr<int> i_ptr1{new int{5}}; // Not recommendate auto i_ptr2 = std::make_unique<int>(5); // More conviniently template <typename T> struct demo { T m_var; demo(T var) : m_var(var){}; }; auto i_ptr3 = std::make_shared<demo<uint32_t>>(4);
- ISO CPP guidelines suggest avoiding the call of
new
anddelete
explicitly by the rule of no naked new. - I have written a more detailed article on smart pointers here.
9. nullptr
- C++11 introduces a new null pointer type designed to replace C's
NULL
macro. -
nullptr
itself is of typestd::nullptr_t
and can be implicitly converted into pointer types, and unlikeNULL
, not convertible to integral types exceptbool
.
void foo(int); void foo(char*); foo(NULL); // error -- ambiguous foo(nullptr); // calls foo(char*)
10. Strongly-typed enums
enum class STATUS_t : uint32_t { PASS = 0, FAIL, HUNG }; STATUS_t STATUS = STATUS_t::PASS; STATUS - 1; // not valid anymore from C++11
- Type-safe enums that solve a variety of problems with C-style enums including implicit conversions, arithmetic operations, inability to specify the underlying type, scope pollution, etc.
11. Typecasting
- C style casting only change the type without touching underlying data. While older C++ was a bit type-safe and has a feature of specifying type conversion operator/function. But it was implicit type conversion, from C++11, conversion functions can now be made explicit using the
explicit
specifier as follows.
struct demo { explicit operator bool() const { return true; } }; demo d; if (d); // OK calls demo::operator bool() bool b_d = d; // error: cannot convert 'demo' to 'bool' in initialization bool b_d = static_cast<bool>(d); // OK, explicit conversion, you know what you are doing
- If the above code looks alien to you, I have written a more detailed article on C++ typecasting here.
12. Move semantics
- When an object is going to be destroyed or unused after expression execution, then it is more feasible to move resource rather than copying it.
- Copying includes unnecessary overheads like memory allocation, deallocation & copying memory content, etc.
- Consider the following swap function:
template <class T> swap(T& a, T& b) { T tmp(a); // we now have two copies of a a = b; // we now have two copies of b (+ discarded a copy of a) b = tmp; // we now have two copies of tmp (+ discarded a copy of b) }
- using move allows you to swap the resources instead of copying them around:
template <class T> swap(T& a, T& b) { T tmp(std::move(a)); a = std::move(b); b = std::move(tmp); }
- Think of what happens when
T
is, say,vector<int>
of size n. And n is too big. - In the first version, you read and write 3*n elements, in the second version you basically read and write just the 3 pointers to the vectors' buffers, plus the 3 buffers' sizes.
- Of course, class
T
needs to know how to do the moving; your class should have a move-assignment operator and a move-constructor for classT
for this to work. - This feature will give you a significant boost in the performance which is why people use C++ for(i.e. last 2-3 drops of speed).
13. Forwarding references
- Also known (unofficially) as universal references. A forwarding reference is created with the syntax
T&&
whereT
is a template type parameter, or usingauto&&
. This enables two major features- move semantics
- And perfect forwarding, the ability to pass arguments that are either lvalues or rvalues.
Forwarding references allow a reference to binding to either an lvalue or rvalue depending on the type. Forwarding references follow the rules of reference collapsing:
-
T& &
becomesT&
-
T& &&
becomeT&
-
T&& &
becomesT&
-
T&& &&
becomesT&&
Template type parameter deduction with lvalues and rvalues:
// Since C++14 or later: void f(auto&& t) { // ... } // Since C++11 or later: template <typename T> void f(T&& t) { // ... } int x = 0; f(0); // deduces as f(int&&) f(x); // deduces as f(int&) int& y = x; f(y); // deduces as f(int& &&) => f(int&) int&& z = 0; // NOTE: `z` is an lvalue with type `int&&`. f(z); // deduces as f(int&& &) => f(int&) f(std::move(z)); // deduces as f(int&& &&) => f(int&&)
- If this seems complex & weird to you then read this first & then come back here.
14. Variadic templates
void print() {} template <typename First, typename... Rest> void print(const First &first, Rest &&... args) { std::cout << first << std::endl; print(args...); } print(1, "lol", 1.1);
- The
...
syntax creates a parameter pack or expands one. A template parameter pack is a template parameter that accepts zero or more template arguments (non-types, types, or templates). A template with at least one parameter pack is called a variadic template.
15. constexpr
constexpr uint32_t fibonacci(uint32_t i) { return (i <= 1u) ? i : (fibonacci(i - 1) + fibonacci(i - 2)); } auto fib_5th_term = fibonacci(6); // equal to `auto fib_5th_term = 8`
- Constant expressions are expressions evaluated by the compiler at compile-time. In the above case,
fibonacci
the function is executed/evaluated by the compiler at the time of compilation & result will be substituted at calling the place. - I have written a detailed article on when to use const vs constexpr in C++.
16. Deleted & Defaulted functions
struct demo { demo() = default; }; demo d;
- Now you might be wondering that rather than writing 8+ letters(i.e.
= default;
), I could simply use {} i.e. empty constructor. That's true! but think about copy constructor, copy assignment operator, etc. - An empty copy constructor, for example, will not do the same as a defaulted copy constructor (which will perform a member-wise copy of its members).
You can limit certain operation or way of object instantiation by simply deleting the respective method as follows
class demo { int m_x; public: demo(int x) : m_x(x){}; demo(const demo &) = delete; demo &operator=(const demo &) = delete; }; demo obj1{123}; demo obj2 = obj1; // error -- call to deleted copy constructor obj2 = obj1; // error -- operator= deleted
In older C++ you have to make it private. But now you have delete
compiler directive.
17. Delegating constructors
struct demo { int m_var; demo(int var) : m_var(var) {} demo() : demo(0) {} }; demo d;
- In older C++, you have to create common initialization member function & need to call it from all the constructor to achieve the common initialization.
- But from C++11, now constructors can call other constructors in the same class using an initializer list.
18. Lambda expression
auto generator = [i = 0]() mutable { return ++i; }; cout << generator() << endl; // 1 cout << generator() << endl; // 2 cout << generator() << endl; // 3
- I think this feature no need any introduction & hot favourite among other features.
- Now you can declare functions wherever you want. That too with zero cost performance impact.
- I wrote a separate article to learn lambda expression in C++ with example.
19. Selection statements with initializer
- In earlier C++, the initializer is either declared before the statement and leaked into the ambient scope, or an explicit scope is used.
-
With C++17, the new form of
if/switch
can be written more compactly, and the improved scope control makes some erstwhile error-prone constructions a bit more robust:
switch (auto STATUS = window.status()) // Declare the object right within selection statement { case PASS:// do this break; case FAIL:// do that break; }
- How it works
{ auto STATUS = window.status(); switch (STATUS) { case PASS: // do this break; case FAIL: // do that break; } }
20. std::tuple
auto employee = std::make_tuple(32, " Vishal Chovatiya", "Bangalore"); cout << std::get<0>(employee) << endl; // 32 cout << std::get<1>(employee) << endl; // "Vishal Chovatiya" cout << std::get<2>(employee) << endl; // "Bangalore"
- Tuples are a fixed-size collection of heterogeneous values. Access the elements of a
std::tuple
by unpacking usingstd::tie
, or usingstd::get
. - You can also catch arbitrary & heterogeneous return values as follows:
auto get_employee_detail() { // do something . . . return std::make_tuple(32, " Vishal Chovatiya", "Bangalore"); } string name; std::tie(std::ignore, name, std::ignore) = get_employee_detail();
- Use
std::ignore
as a placeholder for ignored values. In C++17, structured bindings should be used instead.
21. Class template argument deduction
std::pair<std::string, int> user = {"M", 25}; // previous std::pair user = {"M", 25}; // C++17 std::tuple<std::string, std::string, int> user("M", "Chy", 25); // previous std::tuple user2("M", "Chy", 25); // deduction in action!
- Automatic template argument deduction much likes how it's done for functions, but now including class constructors as well.
Closing words
Here, we have just scratched the surface in terms of new feature & the possibility of its application. There are many things to learn in Modern C++, but still, you can consider this as a good starting point. Modern C++ is not only expanding in terms of syntax but there is lot more other features are also added like unordered containers, threads, regex, Chrono, random number generator/distributor and many new STL algos(like all_of()
, any_of()
and none_of()
, etc).
Happy Modern C++ Coding...!