⬆️ ⬇️

Annotation to "Effective Modern C ++" by Scott Myers

A couple of months ago, Scott Meyers ( Scott Meyers ) has released a new book, Effective Modern C ++ . In recent years, he is undoubtedly the No. 1 writer “about this,” besides, he is a brilliant lecturer and each of his new books is simply doomed to be read by C ++ writers. Moreover, I waited for just such a book for a long time, the C ++ 11 standard came out, C ++ 14 followed it, C ++ 17 is already seen ahead, the language is changing rapidly, but nowhere have the changes been described in general, the relationship between these are dangerous places and recommended patterns.



Nevertheless, regularly looking through Habr, I did not find the publication about the new book, it seems I will have to write myself. Of course, there will not be enough of me for a full translation, so I decided to make a brief squeeze, modestly calling it an annotation. I also took the liberty to regroup the material, it seems to me that for a short retelling this order is better suited. All code examples are taken directly from the book, occasionally with my additions.



One warning: Myers does not describe syntax , it is assumed that the reader knows the keywords, how to write a lambda expression, etc. So if someone decides to start learning C ++ 11/14 with this book, he will have to use additional material for reference. However, this is not a problem, everything is googled in one click.



From C ++ 98 to C ++ 11/14. Gallop on all new



auto - at first glance, just a huge spoon of syntactic sugar, which, however, can change if not the essence of the form of C ++ code. It turns out that Straustrup proposed to introduce this keyword (definite, but useless in C) in the current sense as early as 1983, but abandoned this idea under the pressure of the C-community. See how it changes the code:

')

template<typename It> void dwim(It b, It e) { while(b != e) { typename std::iterator_traits<It>::value_type value=*b; .... } } template<typename It> void dwim(It b, It e) { while(b != e) { auto value=*b; ... } } 


The second example is not just shorter, it hides a completely unnecessary exact type of expression here * b, by the way, in strict accordance with the canons of the classical, still pre-template, OOP. Moreover, in fact, the expression std :: iterator_traits <It> :: value_type is nothing more than a brilliant crutch, invented at the beginning of STL to determine the type of iterator resulting from dereference, the first option will work only with the type for which the specialization iterator_traits <> is defined, for the second, only operator * () is needed. Down with crutches!



Do not convince? Here's another example, in my opinion, just deadly:



 std::unorderd_map<std::string,int> m; for(std::pair<std::string,int>& p : m) { ... } 


This code is not compiled,

proof
auto1.cc:8: 8: error: std :: pair <std :: basic_string <int>, int> from <std :: basic_string <pair> int>

, the fact is that the correct type for std :: unordered_map <std :: string, int> is std :: pair < const std :: string, int>, it is obvious that the key must be constant, but it is much easier to use auto than to keep exact type of expression in the head.

A few more points that give rigor to the language:



 int x1=1; //1  int x2; //2    ! auto x3=1; //3  auto x4; //4 !    std::vector<int> v; unsigned x5=v.size(); //5   size_t,    auto x6=v.size(); //6  int f(); int x7=f(); //7     f() ? auto x8=f(); //8  


As you can see from these examples, the systematic use of auto can save a lot of nerves when debugging.



And finally, where it is simply impossible without auto , lambda expressions:



 auto derefUPLess= [](const std::unique_ptr<Widget>& p1, const std::unique_ptr<Widget>& p2) { return *p1 < *p2; }; 


In this case, the exact type of derefUPLess is known only to the compiler, it simply cannot be stored in a variable without using auto . Of course it is possible to write this:



 std::function<bool (const std::unique_ptr<Widget>&, const std::unique_ptr<Widget>&)> derefUPLess= [](const std::unique_ptr<Widget>& p1, const std::unique_ptr<Widget>& p2) { return *p1 < *p2; }; 


however, std :: function <> and lambda are not of the same type , it means that a constructor will be called, possibly with memory allocation on the heap, besides the call to std :: function <> is guaranteed more expensive than calling the lambda function directly.

And finally - a fly in the ointment, auto works differently when initialized through braces:



 int x1=1; int x2(1); int x3{1}; int x4={1}; 


All of these expressions are completely equivalent, however:

 auto x1=1; auto x2(1); auto x3{1}; auto x4={1}; 


x1 and x2 will be of type int , but x3 and x4 will be of a different type, std :: initializer_list <int> . As soon as auto encounters the {} initializer, it returns the internal C ++ type for such constructions - std :: initializer_list <> . Why this is so, even Myers admits that he doesn’t know, much less I won’t guess.



decltype - everything is more or less simple, this construct was added to make it more convenient to write templates, in particular, functions with a return type depending on the template parameter:



 template<typename Container, typename Index> auto access(Container& c, Index i) -> decltype(c[i]) { .... return c[i]; } 


Here auto simply indicates that the return type will be specified after the function name, and decltype () determines the type of the return value, as a rule, a reference to the i-th element of the container, however, in the general case, exactly what returns c [i], whatever it is .

uniform initialization - as the name implies, the new standard tried to introduce a universal way to initialize variables, and this is fine, for example, you can now write like this:



 std::vector<int> v{1,2,3}; //    sockaddr_in sa={AF_INET, htons(80), inet_addr("127.0.0.1")}; 


Moreover, using braces you can even initialize non-static members of the class (regular brackets do not work):



 class Widget { ... int x{0}; int y{0}; int z{0}; }; 


It is also finally hiding evergreen rakes in the closet that were always lying under their feet, especially annoying template developers:



 Widget w1(); //      , //    Widget w2{}; //          


And one more step to the strictness of the language, the new initialization prevents the transformation of types with a loss of accuracy (narrowing conversion):



 double a=1, b=2; int x=a+b; // fine int y={a+b}; // error 


However ..., it still does not leave the feeling that something went wrong. First, where curly brackets are involved, initialization always occurs through the internal type std :: initializer_list <> , but for some reason, if the class defines one of the constructors with this parameter, this constructor is always preferred by the compiler . For example:



 class Widget { Widget(int, int); Widget(std::initializer_list<double>); }; Widget w1(0, 0); // calls ctor #1 Widget w2{0, 0}; // calls ctor #2 !? 


Contrary to all obviousness, in the second case, the compiler will ignore the ideally suitable constructor_1 and call constructor_2, converting int to double. By the way, if you swap the int and double types in the class definition, the code will generally stop compiling because the conversion of {double, double} to std :: initializer_list <int> occurs with loss of precision.



This conflict can occur with any code now, according to the rules of C ++ 11.

std :: vector (10, 20) creates an object of 10 elements, whereas

std :: vector {10, 20} creates an object of only two elements.

From above, we will decorate all this with a branch of dill - for copy constructors and move constructors this rule does not work:



 class Widget { Widget(); Widget(const Widget&); Widget(Widget&&); Widget(std::initializer_list<int>); operator int() const; }; Widget w1{}; Widget w2{w1}; Widget w3{std::move(w1)}; 


Literally following the letter of the law, one would expect the compiler to select a constructor with the std :: initializer_list parameter and the actual parameters will be converted via the int () operator, so no! In this case (copy / move constructor) copy constructors are called.



In general, the recommendation to always use one type of brackets, round or curly, does not work at all. Myers advises to adhere to one method, applying another only where necessary, he himself leans towards parentheses, in which I agree with him. There remains, however, a problem with templates, where what should be caused is determined by the parameters of the template ... Well, at least C ++ remains a boring language.



nullptr - there's nothing even to talk about here, it is obvious that NULL as well as the value 0 are not pointers , which leads to numerous errors when calling overloaded functions and implementing templates. In this case, nullptr is a pointer and does not lead to any errors.

alias declaration vs typedef

Instead of the usual types of announcements

 typedef std::unique_ptr<std::unordered_map<std::string,std::string>> UPtrMapSS; 


It is proposed to use this design

 using UPtrMapSS=std::unique_ptr<std::unordered_map<std::string,std::string>>; 


these two expressions are absolutely equivalent, but the story does not end there, synonyms (aliases) can be used as templates (alias templates) and this gives them additional flexibility

 template<typename T> using MyAllocList=std::list<T, MyAlloc<T>>; MyAllocList<Widget> lw; 


In C ++ 98, to create such a construct, MyAllocList would have to be declared a template structure, declare the type inside it and use it like this:

 MyAllocList<Widget>::type lw; 


but the story continues. If we use the type declared through typedef as a dependent type inside the template class, we have to use additional keywords

 template<typename T> class Widget { typename MyAllocList<T>::type lw; ... 


in the new syntax everything is much simpler

 template<typename T> class Widget { MyAllocList<T> lw; ... 


In general, metaprogramming promises to be much easier with this syntactic construction. Moreover, starting from C ++ 14, appropriate synonyms are introduced in <type_traits>, that is, instead of the usual

 typename remove_const<...>::type //   remove_const_t<...> 


Using synonyms is an extremely useful habit that you should start cultivating in yourself right now. At one time, typedef ruthlessly dealt with macros, we will not forget, we will not forgive, and we will repay him with the same coin.

scoped enums is another step towards the inner harmony of the language. The fact is that the classical enumerations ( enums ) were announced inside the block, but their visibility ( scope ) remained global.

 enum Color { black, white, red }; 


Black, white and red are visible in the same block as Color, which causes conflicts and name space clogging. New syntax:

 enum class Color { black, white, red }; Color c=Color::white; 


looks much more elegant. Only one but - at the same time removed the automatic conversion of transfers to integer types.

 int x=Color::red; //  int y=static_cast<int>(Color::white); // ok 


this certainly only adds to the severity of the language, but in the overwhelming majority of the code that I saw enums are somehow converted to int , at least for transfer to switch or output to std :: cout .

override, delete and default are new useful words when declaring functions.

override signals to the compiler that this virtual member function of a class must override a certain function of the base class and, if there is no suitable option, it will kindly inform us about the error. Everyone must have come across a situation where a random typo or signature change turns a virtual function into an ordinary one, the most annoying thing is that everything compiles perfectly, but it works somehow wrong. So, this will not happen again. Strongly recommended for use.

delete - is intended to replace the old (and beautiful) trick with the private declaration of the default constructor and the assignment operator. Looks more consistent, but not only. This trick can also be applied to free functions to prevent unwanted argument conversions.

 bool isLucky(int); bool isLucky(char) =delete; bool isLucky(bool) =delete; bool isLucky(double) =delete; isLucky('a'); // error isLucky(true); // error isLucky(3.5); // error 


the same technique can be used for patterns

 template<typename T> void processPointer(T*); template<> void processPointer(void*) =delete; template<> void processPointer(char*) =delete; 


the last two declarations prohibit the generation of functions for some types of argument.

default - this modifier causes the compiler to generate automatic class functions, and it really has to be used. The automatically generated functions in C ++ 98 included a constructor without parameters, a destructor, a copy constructor and an assignment operator, all of which were created according to well-known rules if necessary. In C ++ 11, a moving constructor and an assignment operator were added, but not only, the rules for creating automatic functions themselves have changed. The logic is simple, the automatic destructor calls the destructors of the class members and the base classes in turn, the copy / move constructor calls the corresponding constructors of its members in turn, and so on. However, if we suddenly decide to define any of these functions manually, then this reasonable behavior does not suit us and the compiler refuses to understand our motives, in this case the moving constructor and the assignment operator will not be automatically created. Of course, this logic is also applicable to the copying pair, but it was decided [for now] to be left as it was for backward compatibility. That is, in C ++ 11 it makes sense to write something like this:

 class Widget { public: Widget() =default; ~Widget() =default; Widget(const Widget&) =default; Widget(Widget&&) =default; Widget& operator=(const Widget&) =default; Widget& operator=(Widget&&) =default; ... }; 


If you later decide to define the destructor, nothing will change, otherwise the transfer functions would simply disappear. The code would continue to compile, but the copying counterparts would be invoked.

noexept - finally the standard recognized that the exception specification in C ++ 98 is inefficient, recognized its use as undesirable ( deprecated ) and put in place one big red flag - noexcept , which declares that the function never throws exceptions. If an exception is still thrown, the program is guaranteed to end; in this case, unlike throw () , even the stack will not necessarily be promoted. The checkbox itself is left for efficiency reasons; not only does the stack not need to be kept ready for promotion, the code generated by the compiler itself may differ. Here is an example:

 Widget w; std::vector<Widget> v; ... v.push_back(w); 


When a new element is added to a vector, sooner or later a situation arises when the entire internal buffer needs to be moved in memory, in C ++ 98 the elements are alternately copied . In the new standard, it would be logical to move the elements of the vector, it is an order of magnitude more efficient, but there is one nuance ... If during the copying process any of the elements throw an exception, the new element will not naturally be inserted, but the vector itself will remain in a normal state. If we moved the elements, some of them were already in the new buffer, some were still in the old one, and it is already impossible to restore the memory to a working state. The output is simple, if in the Widget class the moving assignment operator is declared as noexcept , the objects will be moved, if not, they will be copied.

This concludes this protracted review of the new season.
I deliberately dropped a few items - constexpr , std :: cbegin () , etc. They are quite simple and there’s nothing to talk about. What I would like to discuss is the thesis that constant member functions should be thread-safe, but this, on the contrary, goes beyond the simple addition to the syntax, maybe in the comments it will turn out.





Types, their inference and everything related



Type inference (type deduction) in C ++ 98 was used exclusively in the implementation of templates, the new standard added universal references , the auto and decltype keywords . In most cases, deduction is intuitive, but conflicts happen and then understanding of the mechanisms of work is very helpful. Take this pseudocode:

 template<typename T> void f(ParamType param); f(expr); 


The main thing here is that T and ParamType are in general two different types, for example, ParamType can be const T &. The exact type of T is derived from the implementation of the template from both the actual type of expr and the type of ParamType, several options are possible.



For auto , the type inference rules are exactly the same; in this case, auto plays the role of the T parameter, with one exception that I have already mentioned, if auto sees the expression in curly brackets, then the type std :: initializer_list is output.

In the case of decltype, it is almost always the type that was given to it, in the end it was for this very reason that it was invented. However, one nuance does exist - decltype returns a link for all expressions other than just a name, that is:

 int x=1; decltype(x); // x -,   int decltype((x)); // (x) - ,   int& 


but this is unlikely to affect anyone other than libraries actively using macros.




Re-read the written, something turns out a lot. But the most interesting is still ahead, probably better to be divided into two posts. To be continued.

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



All Articles