⬆️ ⬇️

Annotation to "Effective Modern C ++" by Scott Myers. Part 2

Continuation of the previous post .



image



In this part, we will consider not so much technical changes in C ++, as new approaches to development and the possibilities that new tools of the language provide. From my point of view, the previous post was simply a lingering introduction, whereas here you can discuss a lot.



Lambda expressions - the cherry on the cake



No matter how surprising it sounds, but lambda expressions did not bring new functionality to the language (in the original, expressive power). Nevertheless, their ever wider use is rapidly changing the style of the language, the ease of creating object functions on the fly inspires and it remains only to wait for the widespread distribution of C ++ 14 (which is already there, but still not quite) where lambdas have reached full bloom. Starting from C ++ 14, lambda expressions are assumed to be an absolute replacement for std :: bind , there is no real reason to use it. First, and most importantly, lambdas are easier to read and more clearly express the idea of ​​the author. I will not give here a rather cumbersome code to illustrate, in the original, Myers has plenty of it. Secondly, lambda expressions tend to work faster. The fact is that std :: bind captures and stores a pointer to a function , so the compiler has few chances to inline ( inline ) it, while according to the standard, the function call operator in a closure containing a lambda expression must be embedded, so the compiler remains a bit of work to embed all the lambda expression at the call point. There are a couple of less significant reasons, but they boil down mainly to the shortcomings of std :: bind and I’ll omit them.

')

The main danger when working with lambda expressions is how to capture variables. Probably needless to say that this code is potentially dangerous:

[&](...) { ... }; 


If the lambda closure survives any of the captured local variables, we get a dangling reference and as a result undefined behavior . It is so obvious that I will not give even code examples. Stylistically, a little bit better is this:

 [&localVar](...) { ... }; 


We, at least, control exactly which variables are captured, and we also have a reminder before our eyes. But this does not solve the problem in any way.

The code where lambda is generated on the fly:

 std::all_of(container.begin(), container.end(), [&]() { ... }); 


certainly safe, although Myers even here warns of the danger of copy-paste. In any case, it is always a good habit to always explicitly list the variables that are captured by reference and not use [&].

But this is not the end, let's capture everything by value:

 [=]() { *ptr=... }; 


Oops, the pointer is captured by value, and what, is it easier for us?

But that's not all ...

 std::vector<std::function<bool(int)>> filters; class Widget { ... void addFilter() const { filters.emplace_back([=](int value) { return value % divisor == 0; }); } private: int divisor; }; 


Well, here everything is perfectly safe, I hope?

In vain hope.
Wrong. Completely wrong. Horribly wrong. Fatally wrong. (@ScottMeyers)



The fact is that lambda captures local variables in the scope , it does not care that the divisor belongs to the Widget class, is not in the scope, it will not be captured. Here such code for comparison is not compiled at all:



 ... void addFilter() const { filters.emplace_back([divisor](int value) { return value % divisor == 0; }); ... }; 


So what is captured? The answer is simple, this is captured, the divisor in the code is actually interpreted by the compiler as this-> divisor, and if the Widget goes out of scope, we return to the previous example with a dangling pointer. Fortunately, there is a solution for this problem:



 std::vector<std::function<bool(int)>> filters; class Widget { ... void addFilter() const { auto localCopy=divisor; filters.emplace_back([=](int value) { return value % localCopy == 0; }); } private: int divisor; }; 


By making a local copy of a class variable, we allow our lambda to capture it by value.



Perhaps you will cry, but that's not all! I mentioned a little earlier that lambdas capture local variables in scope , they can also use (i.e. depend on) static objects ( static storage duration ), but they do not capture them . Example:



 static int divisor=...; filters.emplace_back([=](int value) { return value % divisor == 0; }); ++divisor; //       


Lambda does not capture the static divisor variable, but refers to it, you can say (although this is not entirely correct) that the static variable is captured by reference . Everything would be fine, but the [=] icon in the definition of lambda kagbe hinted to us that everything is captured by value, the resulting lambda closure is self-sufficient, it can be stored for a thousand years and passed from function to function and it will work as new ... It's a shame. Do you know which of these conclusions? Do not abuse the [=] icon in the same way as [&], do not be lazy to list all variables and you will be happy.



Now you can laugh, this time all ...



And really everything, more about lambda expressions are essentially nothing to say, you can take and use. Nevertheless, I will tell in the rest of the additions that C ++ 14 brought, this area is still poorly documented and this is one of the few places where the changes are really profound.



One thing that annoyed me from the very beginning in C ++ 11 lambda expressions is the inability to move (move) the variable inside the closure. With the submission of TP1 and boost, we suddenly realized that the world around us is full of objects that cannot be copied, std :: unique_ptr <>, std :: atomic <>, boost :: asio :: socket, std :: thread, std :: future - the number of such objects is growing rapidly after a simple idea was realized: something that does not lend itself to natural copying is not necessary, but you can always move it. And suddenly such a cruel disappointment, the new language tool does not support this construction.
Of course, there is a reasonable explanation for this.
And at what point does the movement take place? And what about copying the closure itself? etc
however, sediment remains. And finally, C ++ 14 appears which solves these problems in an unexpected and elegant way: init capture .



 class Widget { ... }; auto wptr=std::make_unique<Widget>(); auto func=[wptr=std::move(wptr)]{ return wptr->...(); }; func(); 


We create a new local variable in the lambda header to which we move the required parameter. Pay attention to two interesting points. The first one is that the names of the variables coincide, this is not necessary, but it is convenient and safe, because their scopes are not suppressed . The variable to the left of the = sign is defined only inside the lambda body, while the variable in the expression to the right of = is defined externally and not defined internally. Second, at the same time we were able to capture whole expressions, and not just variables as before. It is perfectly legal and reasonable to write this:



 auto func=[wptr=std::make_unique<Widget>()] { return wptr->...(); }; func(); 


But what do those who remain in C ++ 11 do? Frankly, before the release of this book, I repeatedly tortured the Internet and always received one answer - in C ++ 11 this is not possible, but there is a solution and it is described right in the next paragraph (the Internet should have blush in this place). Recall how this section began: “Lambda expressions did not bring new language into the language,” everything that they do can be done with the same success with their hands. Something like that:



 class PeudoLambda { explicit PeudoLambda(std::unique_ptr<Widget>&& w) : wptr(std::move(w)) {} bool operator()() const { return wptr->...(); } private: std::unique_ptr<Widget wptr ; }; auto func=PeudoLambda(std::make_unique<Widget>()); func(); 


If you still don't want to work with your hands, but you want to use lambda expressions, then ... the solution is just the same, you just have to use std :: bind instead of a homemade class, this is exactly the case when using it in C ++ 11 remains justified.



The trick is performed in two steps: on time, our object is moved to an object created by std :: bind , then a link to this object is transferred to the count of two lambda.



 std::vector<double> data; auto func=[data=std::move(data)] { ... }; // C++14 way auto func=std::bind( [](std::vector<double>& data) { ... }, // C++11 trick std::move(data) ); 


Not so, of course, elegant, but it works the same, as a temporary measure will go quite well.



The second, and most importantly, why everyone waited for C ++ 14 with impatience, introduced template lambda expressions ( generic lambdas ).



 auto f=[](auto x) { return func(x); }; 


Using auto in the parameter declaration we get the ability to pass arbitrary values ​​to the lambda closure. And how does it work under the hood? Nothing magical:



 class PseudoLambda { ... template<typename T> auto operator()(T x) const { return func(x); } }; 


Just the corresponding operator () is declared a template and accepts any types. However, the given example is not entirely correct, the lambda in this example will always pass x as an lvalue, even if the parameter to the lambda is transmitted as an rvalue. Here it would be useful to re-read the first part of the post about type inference, and even better the corresponding chapter in the book. I, however, immediately give the final version:



 auto f=[](auto&& x) { return func(std::forward<decltype(x)>(x)); }; //       // , ,        auto f=[](auto&&... x) { return func(std::forward<decltype(x)>(x)...); }; 


Well, that's probably all about lambda. I predict that soon there will be a lot of elegant code that still uses unconscious possibilities of lambda expressions, something like the explosive growth of metaprogramming. I stock up on popcorn.



Smart pointers, Smart pointers



Dangerous topic, mountains of paper are written on this topic, thousands of young commentators with burning eyes are mercilessly banned on various forums. However, trust in Myers, he promises, literally
“I will focus on information that is often not in the API documentation, noteworthy usage examples, analysis of execution speed, etc. Owning this information means the difference between using and efficiently using smart pointers. ”
Under such guarantees, I would rather venture into this raging holivar.

In modern language, starting from C ++ 11, there are three types of smart pointers, std :: unique_ptr, std :: shared_ptr <> and std :: weak_ptr <> , all of them work with objects located on the heap, but each of them implements its own model manage your data.





std :: shared_ptr <> is the most famous of this triad, however, since it uses internal reference counters for an object, it is noticeably less efficient than ordinary pointers. Fortunately, due to the simultaneous appearance of atomic variables, operations with std :: shared_ptr <> are completely thread-safe and almost as fast as regular pointers. However, when creating a smart pointer, the memory on the heap should be allocated not only for storing the object itself, but also for the control unit , in which reference counters and a reference to the allocator are stored. Allocating this memory greatly affects the speed of execution and this is a very good reason to use std :: make_shared <> () instead of creating the pointer with your hands, the latter function allocates memory for both the object and the control block at a time and therefore greatly benefits in speed. Nevertheless, std :: shared_ptr <> is naturally twice as large as a simple pointer, not counting the memory allocated on the heap.

std :: shared_ptr also supports non-standard memory allocators (normal delete by default), and such a nice design: the pointer type does not depend on the presence of the allocator and its signature.



 std::shared_ptr<Widget> p1(new Widget(...), customDeleter); std::shared_ptr<Widget> p2=std::make_shared<Widget>(....); 


these two pointers are of the same type and can be assigned to each other, transferred to the same function, placed together in a container, although it is very flexible to allocate memory on the heap. Unfortunately, std :: make_shared does not support nonstandard deallocators, you have to create with your hands.

I also want to note that in C ++ std :: shared_ptr <> implements the concept of a garbage collector , the object will be destroyed when the last of its pointers no longer refers to it, and, unlike collectors in other languages, the destructor is called immediately and deterministic .

Obviously, when working with a shared pointer, there is only one danger - to pass a raw pointer to the constructors of two different classes:



 Widget *w=new Widget; std::shared_ptr<Widget> p1(w); std::shared_ptr<Widget> p2(w); 


In this case, two control blocks with their reference counters will be created and, inevitably, sooner or later, two destructors will be called. The situation is avoided simply, you should never use raw pointers to an object, ideally it is always better to use std :: make_shared <> () . However, there is an important exception:



  std::vector<std::shared_ptr<Widget>> widgetList; class Widget { ... void save() { widgetList.emplace_back(this); } }; 


Here, Widget wants to insert itself into some external container, for which it needs to create a shared pointer. However, the class object does not know, and cannot in principle know whether it has already been transferred under the control of another pointer, if so, this code will inevitably fall. To resolve this situation, a CRTP class std :: enable_shared_from_this was created:



  std::vector<std::shared_ptr<Widget>> widgetList; class Widget : public td::enable_shared_from_this<Widget> { ... void save() { widgetList.emplace_back(shared_from_this()); } }; 


In a magical way, the inherited function shared_from_this () will find and use the control block of the class, this is equivalent to copying a smart pointer if it was created, or creating it if it was not.

In general, this is a great class - powerful, compact, extremely fast for its functionality. The only thing that can be reproached is that it is omnipresent, it is used where it is necessary, where it is not necessary, and where it is by no means necessary.



std :: unique_ptr <> on the contrary, is greatly underused in my opinion. Just take a look at its characteristics - it takes exactly the same amount of memory as a regular pointer, its instructions are almost always translated to the exact same code as for a regular pointer. This hints at the fact that it would be nice to think about your design, if the pointer is the only owner of the object at any given moment, then std :: unique_ptr <> is undoubtedly the best candidate. Of course, thinking in terms of sharing / not sharing is not very familiar yet, but you also had to get used to the systematic use of const .

A few more buns in the bundle, std :: unique_ptr <> is freely convertible (naturally moved ) to std :: shared_ptr <> , there is no way back, even if the reference count is 1.

 auto del=[](base_type* p) { ...; delete p; }; template<typename... Ts> std::unique_ptr<base_type, decltype(del)> factory(Ts&&... args) { std::unique_ptr<base_type, decltype(del)> p(nullptr, del); ... p.reset(new derived_type(std::forward<Ts>(args)...)); //    std::unique_ptr<> //         //     return p; } //       //     std::shared_ptr<base_type>=factory(...args...); 


From the example, another bun is visible - std :: unique_ptr <derived_class> is freely converted to std :: unique_ptr <base_class> . In general, an abstract factory is a natural pattern of use for this type of pointer.

More buns can be initialized with an incomplete type ( pimpl idiom ), a convenient option for fans of this style. And also, if you declare std :: unique_ptr <> constant, you cannot pass it up from the scope where it was created.

You can also create std :: unique_ptr <> with a non-standard memory allocator, but unlike std :: shared_ptr <>, this affects its type:

 void del1(Widget*); void del2(Widget*); std::unique_ptr<Widget, decltype(del1)> p1; std::unique_ptr<Widget, decltype(del2)> p2; 
here, p1 and p2 are two different types, this is the price you have to pay for the minimum size of the object, in addition, non-standard distributors are also not supported by std :: make_unique .

And finally, the bun of which is extremely not recommended to use , unique pointers can be of the form std :: unique_ptr <T []> which can store an array, nonstandard distributors are incompatible with it, and generally, there are enough other types of containers in C ++.

This is the most vivid example of the type for which copying does not make sense in design, but moving the other way around is a natural operation.



std :: weak_ptr <> is a superstructure over std :: shared_ptr <> and, oddly enough, it sounds, it cannot be dereferenced, i.e. the data he points to is not available. Two almost single operations on it are a constructor from a shared pointer and conversion to a shared pointer.

 auto sp=std::make_shared<Widget>(); std::weak_ptr<Widget> wp(sp); ... std::shared_ptr<Widget> sp1=wp; // 1 std::shared_ptr<Widget> sp2=wp.lock(); // 2 


That is, we can create a weak pointer from the shared one, store it for a while, and then try to get the shared pointer out of it again. What for? The fact is that std :: weak_ptr <> does not own the object to which it points, either individually as std :: unique_ptr <> , or cooperatively as std :: shared_ptr <> . It only refers to this object and gives us the opportunity to atomic gain control over it, that is, to create a new shared pointer that owns this object. Naturally, by this time the object may already be destroyed, hence the two options in the example. The first, through the constructor, will throw an exception std :: bad_weak_ptr in this case. The second option is softer, std :: weak_ptr <> :: lock () will return an empty shared pointer, but you should not forget to check it before using.

And what is it for? For example, to store loadable objects in a temporary container cache, we don’t want to store the object forever in memory, but we don’t want to load the object every time it is needed, so we store the links as weak pointers and when prompted, if the pointer is hung, we load the object again, and if the object has already been loaded and has not yet been deleted, use the resulting shared pointer. There is still a situation when two objects must refer to each other, refer to what? The answer “simple pointers” is not accepted because their fundamental limitation is not to know what is there with the object you are pointing to, and if we use shared pointers, then this couple will permanently freeze in memory, keeping each other’s reference counters. The solution is to use shared_ptr at one end and weak_ptr at the other, then nothing will keep the first object from being destroyed, and the second will be able to determine this.

In general, although weak pointers are not very common, they make the picture complete and cover all holes in the application.



On this, let me take this chapter as closed, there really is a lot written about smart pointers, I’d hardly add something new, even retelling Myers.



Universal links



On this topic, Myers continuously writes in a blog and lectures the past two years. The concept itself is so amazing that it changes the usual methods of programming and working with objects. However, there is something in it that doesn’t fit well in the head, at least in mine, so I ask the community for permission to start from the very beginning, from elementary foundations. At the same time, I can give my thoughts in order.



Recall what is lvalue and rvalue , the terms that are almost more years old than C ++. It is relatively simple to define lvalue : this is all that can stand to the left of the assignment sign '='. For example, all names are automatically lvalue. But with rvalue it is much more foggy, as if everything that is not an lvalue , that is, can stand to the right of '=', but cannot stand to the left. And what can not stand on the left? Please read the entire list: well, firstly, naturally, literals, and secondly, the results of expressions not assigned to any variable, that intermediate result in the expression x = a + b; which has been computed and will be assigned to x (this is easier to understand if you think of x not as a whole but as a complex class, obviously, the right part is first calculated and then the assignment operator x is called). However, remember: the name is always lvalue , it really matters.



( , ) , , . , std::map — , std::swap ,
!!
stl , . , std::swap .
, std::map std::map , , std::swap , . , () - , ? — . rvalue references &&. type&& , type , type& type* , , , .. , .



 int x1=0; int&& x2=0; // assigning lvalue to rvalue int&& x3=x1; // error: cannot bind 'int' lvalue to 'int&&' int&& x4=std::move(x1); // x4 is a name, so it is lvalue here int&& x5=x4; // error: cannot bind 'int' lvalue to 'int&&' auto&& x6=0; auto&& x7=x1; 
— type&& ( rvalue reference ) type .
, , .


, type&& rvalue reference , (type deduction) , auto , , (universal references) .



 template<typename T> void f(T&& param); auto&& var2 = var1; 


( ), param var2 .



 Widget w; f(w); // 1 f(std::move(w)); //2 


param — , lvalue (lvalue reference) — Widget&. rvalue — Widget&&.



T&& (, o T& T&&), T&&. :



 template<typename T> void f(std::vector<T>&& param); template<typename T> void f(const T&& param); 


param rvalue reference , , : «cannot bind lvalue to rvalue» , lvalue reference . T&& :



 template<class T> class vector { public: void push_back(T&& x); ... }; 


, push_back T&& , , , rvalue .



C++14 - rvalue references universal references auto&& .



, std::move std::forward , :



 template<typename T> decltype(auto) move(T&& param) { return static_cast<remove_reference_t<T>&&>(param); } template<typename T> T&& forward(T&& param) { return static_cast<T&&>(param); } 


, std::move && std::remove_reference_t [C++14] .. T&&, rvalue reference . , std::forward , lvalue reference lvalu rvalue reference , .



name ( ), rvalue



 class Widget { std::string name; public: .... Widget(Widget&& x) : name(std::move(x.name)) {} template<typename T> void setName(T&& _name) { name=std::forward<T>(_name); } }; 
, setName() , rvalue lvalu , <i<std::forward . std::move , , .

success-story : C++98 :



 std::set<std::string> names; void add(const std::string& name) { ... names.insert(name); } std::string name(" "); add(name); // 1 pass lvalue std::string add(std::string("")); // 2 pass rvalue std::string add(""); // 3 pass string literal 


, . , . — const char* , , . . .



, :



 template<typename T> void add(T&& name) { ... names.emplace(std::forward<T>(name)); } std::string name(" "); add(name); // 1 pass lvalue std::string add(std::string("")); // 2 pass rvalue std::string add(""); // 3 pass string literal 


, , std::set::emplace() . .



, - , , ( perfect forwarding ). C++ . std::move - : « » ( , ). C++ . 19..-, C++ , . , .



API



, . , - , : C++ Concurrency in Action . .



, . , .

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



All Articles