📜 ⬆️ ⬇️

Ten C ++ 11 features that every C ++ developer should use

This article discusses a number of C ++ 11 features that all developers should know and use. There are many new additions to the language and the standard library, this article only superficially covers some of them. However, I believe that some of these new features should be mundane for all C ++ developers. There are probably a lot of similar articles, in this I will try to make a list of possibilities that should be included in everyday use.

Today in the program:

# 1 - auto


Prior to C ++ 11, the auto keyword was used as a variable storage specifier (such as register, static, extern ). In C ++ 11, auto allows you to not explicitly specify the type of a variable, telling the compiler to define the actual type of the variable itself, based on the type of the value being initialized. This can be used to declare variables in different scopes, such as namespaces, blocks, loop initialization, and so on.
 auto i = 42; // i - int auto l = 42LL; // l - long long auto p = new foo(); // p - foo* 

Using auto allows you to reduce the code (unless, of course, the type is not int , which is one letter less). Think about the STL iterators that you should always write to pass containers. Thus, this makes obsolete the typedef definition just for the sake of simplicity.
 std::map<std::string, std::vector<int>> map; for(auto it = begin(map); it != end(map); ++it) { // do smth } // ,  ++03  ++11 // C++03 for (std::vector<std::map<int, std::string>>::const_iterator it = container.begin(); it != container.end(); ++it) { // do smth } // C++11 for (auto it = container.begin(); it != container.end(); ++it) { // do smth } 

It is worth noting that the return value cannot be auto . However, you can use auto instead of the return type of the function. In this case, auto does not tell the compiler that it must determine the type, it only instructs it to look for the return type at the end of the function. In the example below, the return type of the compose function is the return type of the + operator, which summarizes values ​​of type T and E
 template <typename T, typename E> auto compose(T a, E b) -> decltype(a+b) // decltype -        { return a+b; } auto c = compose(2, 3.14); // c - double 


# 2 - nullptr


Previously, the NULL macro was used to reset the pointers, which is zero - an integer type, which naturally caused problems (for example, overloading functions). The nullptr has its own std::nullptr_t , which saves us from past problems. There are implicit conversions of nullptr to a null pointer of any type and to bool (as false ), but there is no conversion to integer types.
 void foo(int* p) {} void bar(std::shared_ptr<int> p) {} int* p1 = NULL; int* p2 = nullptr; if(p1 == p2) {} foo(nullptr); bar(nullptr); bool f = nullptr; int i = nullptr; // :    int   reinterpret_cast 

# 3 - range-based loops


In C ++ 11, foreach paradigm support has been added to iterate over the set. In the new form, it is possible to perform iterations if the begin() and end() methods are overloaded for the iteration object.
')
This is useful when you just want to get the elements of an array / container or do something with them, without worrying about indexes, iterators, or number of elements.
 std::map<std::string, std::vector<int>> map; std::vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); map["one"] = v; for(const auto &kvp: map) { std::cout << kvp.first << std::endl; for(auto v: kvp.second) std::cout << v << std::endl; } int arr[] = {1,2,3,4,5}; for(int &e: arr) e *= e; 

# 4 - override and final


I have always disliked virtual functions in C ++. The virtual optional and therefore made it a bit difficult to read the code, forcing it to always return to the top of the inheritance hierarchy to see if one method or another was declared virtual. I have always used this keyword also in derived classes (and encouraged people who did it) to make the code more understandable. However, there are errors that can still occur. Take the following example:
 class B { public: virtual void f(short) {std::cout << "B::f" << std::endl;} }; class D : public B { public: virtual void f(int) {std::cout << "D::f" << std::endl;} }; 

D::f overrides B::f . However, they have different signatures, one method accepts short , the other one - int , therefore B::f is just another method with the same name, overloaded, not overdetermined. Thus, working through a pointer to the base class, you can call f() and wait for the output of the “overridden” method: “D :: f”, but the output will be “B :: f”.

Here is another possible error: the parameters are the same, but in the base class the method is constant, and in the derivative it is not.
 class B { public: virtual void f(int) const {std::cout << "B::f " << std::endl;} }; class D : public B { public: virtual void f(int) {std::cout << "D::f" << std::endl;} }; 

Again, these are two overloaded functions, not redefined functions.
Fortunately, now there is a way to get rid of these errors. Two new identifiers have been added (not keywords): override to indicate that the method is a virtual method override in the base class and final indicating that the derived class should not override the virtual method. The first example now looks like this:
 class B { public: virtual void f(short) {std::cout << "B::f" << std::endl;} }; class D : public B { public: virtual void f(int) override {std::cout << "D::f" << std::endl;} }; 

Now this will cause a compilation error (in the same way, if you used override in the second example):
 'D::f': method with override specifier 'override' did not override any base class methods 

On the other hand, if you want to make a method that is not intended to be redefined (lower in the hierarchy), it should be noted as final . You can use both identifiers in a derived class at once.
 class B { public: virtual void f(int) {std::cout << "B::f" << std::endl;} }; class D : public B { public: virtual void f(int) override final {std::cout << "D::f" << std::endl;} }; class F : public D { public: virtual void f(int) override {std::cout << "F::f" << std::endl;} }; 

A function declared as final cannot be overridden by the function F::f() - in this case, it overrides the base class method ( ) for class D

# 5 - strictly typed enum


The "traditional" enumerations in C ++ have some drawbacks: they export their values ​​to the surrounding scope (which can lead to name conflicts), they are implicitly converted to an integer type and cannot have a user-defined type.

These problems are fixed in C ++ 11 with the introduction of a new category of enums, called strongly-typed enums . They are defined by the enum class keyword. They no longer export their enumerated values ​​to the surrounding scope, are no longer implicitly converted to an integer type, and may have a user-defined type (this option is also added for "traditional" enumerations ").
 enum class Options {None, One, All}; Options o = Options::All; 


# 6 - smart pointers


There are many articles both on Habré and on other resources written on this topic, so I just want to mention smart pointers with reference counting and automatic memory freeing:
  1. unique_ptr : should be used when the memory resource should not have been shared (it does not have a copy constructor), but it can be transferred to another unique_ptr
  2. shared_ptr : should be used when the memory resource is to be shared
  3. weak_ptr : contains a link to an object that is managed by shared_ptr , but does not count links; allows you to get rid of the cyclic dependence

The following example demonstrates unique_ptr . To transfer ownership of an object to another unique_ptr , use std :: move (this function will be discussed in the last paragraph). After the transfer of ownership, the smart pointer that transferred ownership becomes zero and get() returns nullptr .
 void foo(int* p) { std::cout << *p << std::endl; } std::unique_ptr<int> p1(new int(42)); std::unique_ptr<int> p2 = std::move(p1); // transfer ownership if(p1) foo(p1.get()); (*p2)++; if(p2) foo(p2.get()); 

The second example demonstrates shared_ptr . The usage is similar, although the semantics are different, since ownership is now shared.
 void foo(int* p) { } void bar(std::shared_ptr<int> p) { ++(*p); } std::shared_ptr<int> p1(new int(42)); std::shared_ptr<int> p2 = p1; bar(p1); foo(p2.get()); 

The first ad is equivalent to the following:
 auto p3 = std::make_shared<int>(42); 

make_shared is a function that has the advantage of allocating memory for a shared object and a smart pointer with a single allocation, as opposed to explicitly obtaining shared_ptr through a constructor, where at least two selections are required. Because of this, a memory leak may occur. The following example demonstrates this; a leak can occur if seed() throws an exception.
 void foo(std::shared_ptr<int> p, int init) { *p = init; } foo(std::shared_ptr<int>(new int(42)), seed()); 

This problem is solved using make_shared .
And, finally, an example with weak_ptr . Note that you must get shared_ptr for an object by calling lock() to access the object.
 auto p = std::make_shared<int>(42); std::weak_ptr<int> wp = p; { auto sp = wp.lock(); std::cout << *sp << std::endl; } p.reset(); if(wp.expired()) std::cout << "expired" << std::endl; 

# 7 - lambda


The new standard has finally added support for lambda expressions. You can use lambdas wherever a functor or std::function is expected. Lambda, generally speaking, is a shorter functor notation, something like an anonymous functor. More details can be read, for example, on MSDN .
 std::vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); std::for_each(std::begin(v), std::end(v), [](int n) {std::cout << n << std::endl;}); auto is_odd = [](int n) {return n%2==1;}; auto pos = std::find_if(std::begin(v), std::end(v), is_odd); if(pos != std::end(v)) std::cout << *pos << std::endl; 

Now a little more tricky - recursive lambdas. Imagine a lambda representing the Fibonacci function. If you try to write it using auto , you will get a compilation error:
 auto fib = [&fib](int n) {return n < 2 ? 1 : fib(n-1) + fib(n-2);}; 

 error C3533: 'auto &': a parameter cannot have a type that contains 'auto' error C3531: 'fib': a symbol whose type contains 'auto' must have an initializer error C3536: 'fib': cannot be used before it is initialized error C2064: term does not evaluate to a function taking 1 arguments 

There is a cyclical relationship. To get rid of it, you must explicitly define the type of the function using std::function .
 std::function<int(int)> lfib = [&lfib](int n) {return n < 2 ? 1 : lfib(n-1) + lfib(n-2);}; 

# 8 - non-member begin () and end ()


You probably noticed that in the examples earlier, I used the begin() and end() functions. This is a new addition to the standard library. They work with all STL containers and can be extended to work with any type.

Let's take, for example, the previous example, where I derived a vector and then searched for the first odd element. If std::vector replaced with a C-like array, the code will look like this:
 int arr[] = {1,2,3}; std::for_each(&arr[0], &arr[0]+sizeof(arr)/sizeof(arr[0]), [](int n) {std::cout << n << std::endl;}); auto is_odd = [](int n) {return n%2==1;}; auto begin = &arr[0]; auto end = &arr[0]+sizeof(arr)/sizeof(arr[0]); auto pos = std::find_if(begin, end, is_odd); if(pos != end) std::cout << *pos << std::endl; 

With begin() and end() it can be rewritten as follows:
 int arr[] = {1,2,3}; std::for_each(std::begin(arr), std::end(arr), [](int n) {std::cout << n << std::endl;}); auto is_odd = [](int n) {return n%2==1;}; auto pos = std::find_if(std::begin(arr), std::end(arr), is_odd); if(pos != std::end(arr)) std::cout << *pos << std::endl; 

This is almost identical to the code with std::vector . Thus, we can write one universal method for all types, which are supported by the begin() and end() functions.
 template <typename Iterator> void bar(Iterator begin, Iterator end) { std::for_each(begin, end, [](int n) {std::cout << n << std::endl;}); auto is_odd = [](int n) {return n%2==1;}; auto pos = std::find_if(begin, end, is_odd); if(pos != end) std::cout << *pos << std::endl; } template <typename C> void foo(C c) { bar(std::begin(c), std::end(c)); } template <typename T, size_t N> void foo(T(&arr)[N]) { bar(std::begin(arr), std::end(arr)); } int arr[] = {1,2,3}; foo(arr); std::vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); foo(v); 

# 9 - static_assert and property classes


static_assert verifies assertion at compile time. If the statement is true, then nothing happens. If - false, the compiler displays the specified error message.
 template <typename T, size_t Size> class Vector { static_assert(Size > 3, "Size is too small"); T _points[Size]; }; int main() { Vector<int, 16> a1; Vector<double, 2> a2; return 0; } 

 error C2338: Size is too small see reference to class template instantiation 'Vector<T,Size>' being compiled with [ T=double, Size=2 ] 

static_assert becomes more useful when used with property classes. This is a collection of classes that provide type information at compile time. They are available in the <type_traits> header. There are several types of classes in this header: helper classes, transformation classes, and property classes themselves.
In the following example, the add function is supposed to work only with integer types.
 template <typename T1, typename T2> auto add(T1 t1, T2 t2) -> decltype(t1 + t2) { return t1 + t2; } 

However, when compiling, there will be no error if you write the following:
 std::cout << add(1, 3.14) << std::endl; std::cout << add("one", 2) << std::endl; 

The program simply displays "4.14" and "e". Using static_assert , these two lines will cause an error during compilation.
 template <typename T1, typename T2> auto add(T1 t1, T2 t2) -> decltype(t1 + t2) { static_assert(std::is_integral<T1>::value, "Type T1 must be integral"); static_assert(std::is_integral<T2>::value, "Type T2 must be integral"); return t1 + t2; } 

 error C2338: Type T2 must be integral see reference to function template instantiation 'T2 add<int,double>(T1,T2)' being compiled with [ T2=double, T1=int ] error C2338: Type T1 must be integral see reference to function template instantiation 'T1 add<const char*,int>(T1,T2)' being compiled with [ T1=const char *, T2=int ] 

# 10 - move semantics


This is another important topic covered in C ++ 11. You can write several articles on this topic, not paragraphs, so I will not go deep.

C ++ 11 introduced the concept of rvalue references (specified with &&) to distinguish between a reference to lvalue (an object that has a name) and rvalue (an object that does not have a name). Moving semantics allows changing rvalues ​​(they were previously considered unchanged and did not differ from const T & types).

The class / structure used to have some implicit member functions: the default constructor (if no other constructor is defined), the copy constructor, and the destructor. The copy constructor performs a one-by-one copy of the variables. This means that if you have a class with pointers to some objects, the copy constructor will copy the pointers, not the objects to which they point. If you want to get objects in a copy, and not just pointers to them, you must explicitly describe this in the copy constructor.

The displacement constructor and the displacement assignment operator — these two special functions take the T && parameter, which is rvalue. In fact, they can modify the object.

The following example shows a dummy buffer implementation. The buffer is identified by a name, has a pointer (wrapped in std::unique_ptr ) to an array of elements of type T and a variable containing the size of the array.
 template <typename T> class Buffer { std::string _name; size_t _size; std::unique_ptr<T[]> _buffer; public: // default constructor Buffer(): _size(16), _buffer(new T[16]) {} // constructor Buffer(const std::string& name, size_t size): _name(name), _size(size), _buffer(new T[size]) {} // copy constructor Buffer(const Buffer& copy): _name(copy._name), _size(copy._size), _buffer(new T[copy._size]) { T* source = copy._buffer.get(); T* dest = _buffer.get(); std::copy(source, source + copy._size, dest); } // copy assignment operator Buffer& operator=(const Buffer& copy) { if(this != &copy) { _name = copy._name; if(_size != copy._size) { _buffer = nullptr; _size = copy._size; _buffer = (_size > 0)? new T[_size] : nullptr; } T* source = copy._buffer.get(); T* dest = _buffer.get(); std::copy(source, source + copy._size, dest); } return *this; } // move constructor Buffer(Buffer&& temp): _name(std::move(temp._name)), _size(temp._size), _buffer(std::move(temp._buffer)) { temp._buffer = nullptr; temp._size = 0; } // move assignment operator Buffer& operator=(Buffer&& temp) { assert(this != &temp); // assert if this is not a temporary _buffer = nullptr; _size = temp._size; _buffer = std::move(temp._buffer); _name = std::move(temp._name); temp._buffer = nullptr; temp._size = 0; return *this; } }; template <typename T> Buffer<T> getBuffer(const std::string& name) { Buffer<T> b(name, 128); return b; } int main() { Buffer<int> b1; Buffer<int> b2("buf2", 64); Buffer<int> b3 = b2; Buffer<int> b4 = getBuffer<int>("buf4"); b1 = getBuffer<int>("buf5"); return 0; } 

The default copy constructor and copy assignment operator should be familiar to you. New in C ++ 11 is a move constructor and a move assignment operator. If you execute this code, you will see that when b4 is created, the move constructor is called. In addition, when b1 assigned a value, the assignment operator is called. The reason for this is the value returned by the getBuffer() function - rvalue.

You probably noticed the use of std :: move in the move constructor, when initializing the variable name and the buffer pointer. The name is a string std::string and std::string also implements move semantics. The same goes for unique_ptr . However, if we wrote just _name(temp._name) , the copy constructor would be called. But why in this case the displacement constructor for std::string was not called? The fact is that even if the displacement constructor for Buffer was called with an rvalue, inside the constructor it is still represented as an lvalue. To make it rvalue again and need to use std::move . This function simply turns the lvalue reference into an rvalue.

Instead of conclusion


There are many things in C ++ 11 that can and should be talked about; This article was just one of many possible beginnings. This article introduced a series of language functions and a standard library that every C ++ developer should know. However, for a deeper understanding of all that has been said, this article is not enough; therefore, additional literature cannot be done here.

Source: https://habr.com/ru/post/182920/


All Articles