This article is a translation of the following blog:
https://herbsutter.com/elements-of-modern-c-style/
The C++11 standard provides many useful new features. This article focuses on the features that make C++11 and C++98 look like a new language, because:
- C++11 has changed the style and habit of writing C++ code and the way of designing C++ libraries. For example, you will see more smart pointers that are used as parameters and return values, as well as functions that return large objects by value.
- They are widely used and you can see them in most code. For example, in modern C++ you can see auto keywords almost every five lines of C++ code.
There are some other very good new features of C++11, but first familiarize yourself with the new features described in this article, because these widely used features show why C++11 code is concise, safe and fast, just like other modern mainstream development languages, and its performance is as powerful as traditional C++.
1. Auto
Use auto whenever possible. For two reasons. First, it's obvious that it's very convenient to avoid duplicating the type names that we have declared and that the compiler already knows.
1 // C++98 2 3 map<int,string>::iterator i = m.begin(); 4 5 double const xlimit = config["xlimit"]; 6 7 singleton& s = singleton::instance(); 8 9 // C++11 10 11 auto i = begin(m); 12 13 auto const xlimit = config["xlimit"]; 14 15 auto& s = singleton::instance();
Second, when you encounter a type that you don't know or can't express in language, auto is not only easy to use. For example, most lambda function types, you can't easily spell out their types or even can't write them at all.
// C++98 binder2nd< greater > x = bind2nd( greater(), 42 ); // C++11 auto x = [](int i) { return i > 42; };
Note that using auto does not modify the semantics of the code. The code is still statically typed, and each expression is neat; it just doesn't force us to redundantly declare the name of the type.
Some people are initially afraid of using auto because it feels like we have not declared (re-declared) the type we want, which means that we may suddenly get a different type. If you want to show mandatory type conversion, that's fine; just declare the target type. But in most cases, using auto is enough. It is rare to get another type because of an error. In the case of strong static typing, the compiler will tell you if the type is wrong.
2. Smart pointer, no delete
Always use smart pointers, not native pointers and delete s. Unless you need to implement your own underlying data structure (encapsulating native pointers in classboundaries)
If you know that you are the only owner of another object, use unique_ptr to represent the only ownership. A "new T" expression can quickly initialize an object with this smart pointer, especially unique_ptr. Typical examples are pointers to implementations (Pimpl Idiom):
1 // C++11 Pimpl idiom: header file 2 class widget { 3 public: 4 widget(); 5 // ... (see GotW #100) ... 6 private: 7 class impl; 8 unique_ptr<impl> pimpl; 9 }; 10 11 // implementation file 12 class widget::impl { /*...*/ }; 13 14 widget::widget() : pimpl{ new impl{ /*...*/ } } { } 15 // ...
shared ownership is represented by shared_ptr. It's better to use make_shared to create shared objects.
1 // C++98 2 widget* pw = new widget(); 3 ::: 4 delete pw; 5 6 // C++11 7 auto pw = make_shared<widget>();
Use weak_ptr to break loops and express optionality (such as implementing an object cache)
1 // C++11 2 class gadget; 3 4 class widget { 5 private: 6 shared_ptr<gadget> g; // if shared ownership 7 }; 8 9 class gadget { 10 private: 11 weak_ptr<widget> w; 12 };
If you know that another object has a longer life cycle than yours and you want to observe it, use the raw pointer.
1 // C++11 2 class node { 3 vector<unique_ptr<node>> children; 4 node* parent; 5 public: 6 ::: 7 };
3. Nullptr
nullptr is used to represent a null pointer, instead of using the number 0 or macro NULL to represent a null pointer, because these are ambiguous and can represent both shaping and pointer.
1 // C++98 2 int* p = 0; 3 4 // C++11 5 int* p = nullptr;
4. Range for
Range-based for loops are more convenient for orderly access to elements within a range.
1 // C++98 2 for( vector<int>::iterator i = v.begin(); i != v.end(); ++i ) { 3 total += *i; 4 } 5 6 // C++11 7 for( auto d : v ) { 8 total += d; 9 }
5. Non-member begin and end
Using non-member functions begin(x) and end(x) (not x.begin() and x.end()), because begin(x) and end(x) are extensible and can work with all container types -- even arrays -- is not just for containers that provide STL-style x.begin() and x.end() member functions.
If you are using a non-STL set type that provides iterators but not STL-style x.begin() and x.end(), you can overload its non-member functions begin () and end (), so that you can code in the same style as the STL container. An example is given in the standard: arrays, and object begin and end functions are provided:
1 vector<int> v; 2 int a[100]; 3 4 // C++98 5 sort( v.begin(), v.end() ); 6 sort( &a[0], &a[0] + sizeof(a)/sizeof(a[0]) ); 7 8 // C++11 9 sort( begin(v), end(v) ); 10 sort( begin(a), end(a) );
6. Lambda functions and algorithms
Lambda expressions change the rules of the game. They change the way you code from time to time, which is elegant and fast. Lambda improves the practicability of existing STL algorithms by a hundred times.
New C++ libraries are designed to support lambad expressions (e.g. PPL), and even some libraries need to use libraries (e.g. c++ AMP) by writing lambda expressions.
Here's an example. Find the first element in v that is > X and < Y. In C++11, the simplest and cleanest code is to use standard algorithms.
1 // C++98: write a naked loop (using std::find_if is impractically difficult) 2 vector<int>::iterator i = v.begin(); // because we need to use i later 3 for( ; i != v.end(); ++i ) { 4 if( *i > x && *i < y ) break; 5 } 6 7 // C++11: use std::find_if 8 auto i = find_if( begin(v), end(v), [=](int i) { return i > x && i < y; } );
What if you want to use a loop or a similar language feature that doesn't actually exist in the language? It can be implemented as a template function (library algorithm), thanks to lambda, which is as convenient as using a language feature, but more flexible, because it is indeed a library rather than a fixed language feature.
1 // C# 2 lock( mut_x ) { 3 ... use x ... 4 } 5 6 // C++11 without lambdas: already nice, and more flexible (e.g., can use timeouts, other options) 7 { 8 lock_guard<mutex> hold { mut_x }; 9 ... use x ... 10 } 11 12 // C++11 with lambdas, and a helper algorithm: C# syntax in C++ 13 // Algorithm: template<typename T> void lock( T& t, F f ) { lock_guard hold(t); f(); } 14 lock( mut_x, [&]{ 15 ... use x ... 16 });
Familiarize yourself with lambda and you'll find them useful, not just in c + +, but in several mainstream languages that have gained support and popularity.
7. Move/&&
move is the best way to optimize copies, although it also includes other aspects (like perfect forwarding).
move semantics have changed the way we design API s. We will increasingly design functions as return by value.
1 // C++98: alternatives to avoid copying 2 vector<int>* make_big_vector(); // option 1: return by pointer: no copy, but don't forget to delete 3 ::: 4 vector<int>* result = make_big_vector(); 5 6 void make_big_vector( vector<int>& out ); // option 2: pass out by reference: no copy, but caller needs a named object 7 ::: 8 vector<int> result; 9 make_big_vector( result ); 10 11 // C++11: move 12 vector<int> make_big_vector(); // usually sufficient for 'callee-allocated out' situations 13 ::: 14 auto result = make_big_vector(); // guaranteed not to copy the vector
If you want a more efficient way than copy ing, use move semantics for your type.
8. Unified Initialization and Initialization List
No change: When initializing a non-POD or auto local variable, continue using the familiar = grammar without extra curly braces {}.
1 // C++98 or C++11 2 int a = 42; // still fine, as always 3 4 // C++ 11 5 auto x = begin(v); // no narrowing or non-initialization is possible
In other cases (especially the ubiquitous use of () to construct objects), curly brackets {} are preferable. Using curly braces {} avoids potential problems: you don't suddenly get a narrowing conversions value (for example, float converts to int), you don't occasionally have uninitialized POD member variables or arrays, and you can avoid the odd thing that happens in c++98: your code compiles well, you need variables but you actually declare them. A function, all due to the ambiguity of the C++ declaration grammar, Scott Meyers famously said: "The most distressing interpretation of C++". Parsing problems with new style grammar will disappear.
1 // C++98 2 rectangle w( origin(), extents() ); // oops, declares a function, if origin and extents are types 3 complex<double> c( 2.71828, 3.14159 ); 4 int a[] = { 1, 2, 3, 4 }; 5 vector<int> v; 6 for( int i = 1; i <= 4; ++i ) v.push_back(i); 7 8 // C++11 9 rectangle w { origin(), extents() }; 10 complex<double> c { 2.71828, 3.14159 }; 11 int a[] { 1, 2, 3, 4 }; 12 vector<int> v { 1, 2, 3, 4 };
The new {} grammar works well almost everywhere.
1 // C++98 2 X::X( /*...*/ ) : mem1(init1), mem2(init2, init3) { /*...*/ } 3 4 // C++11 5 X::X( /*...*/ ) : mem1{init1}, mem2{init2, init3} { /*...*/ }
Finally, it is sometimes convenient to pass function parameters without type-named temporary:
void draw_rect( rectangle );
1 // C++98 2 draw_rect( rectangle( myobj.origin, selection.extents ) ); 3 4 // C++11 5 draw_rect( { myobj.origin, selection.extents } );
The only thing I don't like about using curly braces {} is when initializing a non-POD variable, like auto x= begin(v); using curly braces makes the code unnecessarily ugly, because I know it's a class type, so I don't have to worry about shrinking conversions, and modern compilers have optimized additional copies (or extra moves, if the type is move-enabled). .