The road of Architecture_ Effective C + + summary

Posted by jlr2k8 on Fri, 01 Oct 2021 19:52:40 +0200

1, Get used to C++

1.1 clause 01: treat C + + as a language Federation

C + + can be regarded as a multi paradigm programming language, which supports procedural, object-oriented, functional, generic and metaprogramming languages. These capabilities and flexibility make C + + an unparalleled tool.

The simplest way to understand C + + is to regard it as a federal language composed of related languages rather than a single language, composed of four sub languages

  • C language. Blocks, statements, preprocessors, built-in data types, arrays, pointers, etc. come from C
  • Object oriented C + +. That is, C with Classes includes classified (including constructor and destructor), encapsulation, inheritance, polymorphism, virtual function (dynamic binding), etc
  • Template C + +, that is, the part of C + + generic programming. Template related considerations and design have permeated the whole C + +, and the special clause "only template applies" in the good programming code is not uncommon.
  • STL, STL is a template library, which has excellent close cooperation and coordination with the specifications of containers, iterators, algorithms and function objects.

Note that when switching from one sublanguage to another, it is not surprising to change the strategy of efficient programming rules. For example, for built-in types (meeting the requirements of C language sub) Generally speaking, pass by value is more efficient than pass by reference, but it is better to move from C part of C + + to object oriented C + +. Due to the existence of user-defined constructors and destructors, pass by reference to const is often better. The same is true with template C + +, because the actual type of processing object is not known at that time, so pass by reference is naturally required. However, for STL, the Both iterators and function objects are pointer based, so pass by value is still applicable to STL iterators and function objects.

Therefore, C + + is not an all-in-one language with a set of codes, but is composed of four sub languages, each of which has its own specifications.

1.2 clause 02: try to replace #define with const, enum and inline

This clause is actually a compiler instead of a preprocessor when written down

#define ASPECT_RATIO 1.653 

The name ASPECT_RATIO has never been seen by the compiler. The compiler only obtains 1.653 after being replaced, but it thinks that 1.653 is only a value, not a constant. Maybe the compiler removed it by the preprocessor before processing the source code, so the token name ASPECT_RATIO may not enter the symbol table.

Therefore, when using this constant to obtain a compilation error message, the error message may mention 1.653, but it will never mention ASPECT_RATIO. At this time, if ASPECT_RATIO is defined in the header file, even if the header file is not written by you, you must have no concept of ASPECT_RATIO and never waste time tracking 1.653. At the same time, it will also make debugging difficult because ASPECT_RATIO does not appear in the In the marking table.

Replace the above macro with a constant

const double AspectRatio = 1.653; 

As a language constant, AspectRatio will certainly be seen by the compiler and enter the token table. At the same time, compiler optimization can be carried out, because the preprocessor blindly replaces the macro name with 1.653, which may lead to multiple copies of 1.653 in the object code, while const constant will only generate one memory.

Replace #define with a constant. There are two special cases

  • Define constant pointers. Since constant definitions are often placed in the header file, it is necessary to declare the pointer itself and the object pointed to as const at the same time, that is, write in the header file
const char* const authorName = "Scott Meyers";  // string objects are better than char *  
const std::String author("Scoot Meyers"); 
  • Class specific constant. In order to limit the scope of the constant to class, it must become a member of class; at the same time, in order to ensure that the constant is independent of object, there is only one entity and it must become a static member
class GamePlayer { 
private: 
    static const int NumTurns = 5;    // declare constant  
    int scores[NumTurns];    // Use constant 
}; 

Note that the above is the declaration of NumTurn. The function of declaration is to specify the type and name of variables. Distinguishing between declaration and definition can enable C + + to support separate compilation. Definition is to allocate storage space for variables and may be initialized. Definition is a declaration, because the definition must specify the type and name of variables at the same time, but the declaration is not a definition. The definition of variables in C + + There must be and only once, and variables can be declared multiple times. Variables generally cannot be defined in header files, except const variables.

Generally, the definition is provided in the implementation file, as follows

const int GamePlayer::NumTurns;  // Since the declarative expression has been assigned an initial value, it cannot be assigned again during definition 
 
// Or as follows 
class CostEstimate { 
private: 
    static const double FudgeFactor; // static class constant declaration 
    ...    // In header file 
}; 
 
const double CostEstimate::FudgeFactor = 1.35;    // static class constant definition, located in the implementation file 

Moreover, we can't use #define to create a class specific constant because #define doesn't pay attention to scope. Once a macro is defined, it will be valid for subsequent compilation unless it is #undef somewhere. Therefore, #define can't provide any encapsulation and can't be used as a class specific constant. Of course, const member variables can be encapsulated.

In addition to const, enum can also be used. The behavior of enum is more like #define than const. For example, the const address is legal, but the enum address is illegal. It is generally used to define when a type has multiple constants

class GamePlayer { 
private: 
    enum { NumTurns = 5}; 
    int score[NumTurns]; 
    ... 
};  

#In addition to replacing symbols, define is also used to implement macros. Macros look like functions, but do not incur additional overhead caused by function calls, such as

#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b)) 

This kind of macro must include parentheses for all entities, but even if parentheses are included for all entities, incredible things will happen. I believe I must have been tortured by macro definitions when learning C language.

int a = 5, b = 0; 
CALL_WITH_MAX(++a,b);    // a output 7 
CALL_WITH_MAX(++a,b+1); // a output 9 

The answer is not to write macro functions at all, but to replace them with inline functions

template<typename T> 
inline void callWithMax(const T& a, const T& b) 
{ 
    return a > b ? a:b; 
} 

At this time, inline callWithMax follows the scope and access rules. For example, you can write a private inline function in class. In fact, with const, enum, and inline, macros only need #ifdef/ #ifndef two macro definitions in C + + to prevent repeated compilation.

  1. For simple constants, it is best to replace #define with const object or enum
  2. For macro functions, it is better to replace #define with inline function

1.3 clause 03: use const whenever possible

Const is based on the semantics of constant and read-only. When const modifies a variable, the variable can only produce one copy in memory (constant) and cannot be modified (read-only). Note that when const modifies a class member variable, it represents the constant of object. If it becomes a class constant, you need to use static const.

In addition, const can also modify the pointer, which can modify the pointer itself or the object pointed to by the pointer. Const modification function is essentially modifying the pointer.

char greeting[] = "Hello"; 
char *p = greeting; 
const char* p = greeting;  // const data, non-const pointer 
char* const p = greeting;  // const pointer, non-const data 
const char* const p = greeting; // const pointer, const data 
 
const char *p;    // The meaning is the same. const is on the left of * and the target is constant and cannot be modified 
char const* p; 

If const appears on the left, it means that the target is a constant. If const appears on the right, it means that the pointer itself is a constant.

For STL, the container iterator is actually molded based on the pointer, and similar to the pointer, operators such as *, + +, etc. are set. When the iterator is actually used, it can be regarded as T *.

std::vector<int> vec;    // Through the vector template of int, the iterator of vector is actually a pointer, and other container s may be pointer like classes 
const std::vector<int>::iterator iter = vec.begin(); // iter acts as a T* const pointer 
*iter = 10; 
++ iter;    // Wrong! iter is const 
 
std::vector<int>::const_iterator cIter = vec.begin(); // cIter acts like const T* 
*cIter = 120;    // Error, cIter is const data 
++ cIter;  // correct 

Make the function return a constant value, so that the returned value cannot be modified. Note that const modifies the return value and must also be received with const in the calling function.

class Rational { ... } 
class Rational operator* (const Rational& lhs, const Rational& rhs); 
 
// operator *, the return value is const, which can prevent 
Rational a,b,c; 
(a*b) = c;  // Set const so that this operation cannot be compiled because the return value (const + reference) cannot be changed
 
class TextBlock { 
public: 
    ... 
    const char& operator[] (std::size_t position) const // const function, const return value 
    {    return text[position];    } 
    char& operator[] (std::size_t position)    // Non const object 
    {    return text[position];    } 
private: 
    std::string text; 
} 
 
TextBlock tb("Hello");     
std::cout<<tb[0];    //  
const TextBlock ctb("World");    // Receive with const and modify the return value with const 
std::cout<< ctb[0]; 
tb[0] = 'x';    // ok 
ctb[0] = 'x';    // Error, ctb is const, read-only 
 
 
float Point3d::magnitude3d() const { ... } 
// The following are the conversion steps of internalizing member function into nonmember form 
// 1. Insert an additional parameter into member fuction so that class object can call this function. 
Point3d 
Point3d::magnitude( Point3d *const this ) 
// If the member function is const, it becomes as follows, which also shows that the function modified by const cannot change the member data 
// Point3d *const this the pointer to the address cannot be changed, but the pointed object point3d can be changed. 
// const Point3d *const this the address pointed to by the pointer and the object pointed to cannot be changed. 

Also note that char & needs to return a reference, not char. If char is returned, the actual operation of tb[0] is a copy of tb, and there is no operation on the actual tb. Always pay attention to whether the current variable name of C + + is a reference or a variable.

As mentioned above, const can be applied to member functions. In the in-depth exploration of the C + + object model, const modifier member function actually modifies the object pointed to by the incoming this pointer. Obviously, const modifying the member function causes this object to be consted, so any non static object in the object cannot be modified.

Note that for the pointer object inside the object, the const member function ensures that the pointer cannot be modified, but the object pointed to by the pointer can often be modified.

class CTextBlock { 
public: 
    ... 
    char& operator[] (std::size_t position) const 
    { return pText[position]; } 
private: 
    char* pText; } 
}; 
 
// Although operator [] is decorated with const, the object pointed to by pText can still be modified 
const CTextBlock cctb("Hello"); 
char* pc = &cctb[0]; 
 
*pc = 'J';    // * pc can still be modified, that is, the pc points to the object. Although operator [] is set to const 

Modify the class member variable with the mutable keyword so that it can be modified in the member function modified by const. For example:

class CTextBlock { 
public: 
    ... 
    std::size_t length() const; 
private: 
    char* pText; 
    mutable std::size_t textLength;    // Modified with mutable can be modified in const membership 
    mutable bool lengthIsValid; 
}; 
std::size_t CTextBlock::length() const 
{ 
    if (!lengthIsValid) { 
        textLength = std::strlen(pText); 
        lengthIsValid = true; 
    } 
    return true; 
} 

Note that const function cannot be adjusted to non const function, because const promises not to change the logical state of the object. When const function calls non const function, the object promised not to change is changed. At the same time, const and non const can transform each other.

class TextBlock { 
public: 
    const char& operator[] (std::size_t position) const  
    { 
        return text[positon]; 
    } 
} 
    char& operator[] (std::size_t position) 
    { 
        return const_cast<char&>{   // Remove the return value const 
         static_cast<const TextBlock&>(*this)[position]);    // Add const to * this to call operator[] const 
    } 
}; 

As mentioned above, it is intended to make non const call const. In order to avoid calling itself recursively, it must be clearly pointed out to call const operator []. Therefore, you need to transform * this from the original type TextBlock & to const TextBlock &, and then use const_cast removes const.

summary

  1. Declaring const can help the compiler detect incorrect usage. Const can be applied to objects, function parameters, function return types and member function bodies in any scope

1.4 clause 04: confirm that the object is initialized before being used

It is said in the C + + object model that the rules for C + + to construct objects are to meet the basic needs of the compiler, including synthesizing the default constructor. This often leads to ambiguous initialization of member variables for C + + objects. Generally, just as Clause 1 divides C + + into a set of four languages, initialization may incur run-time costs when using C part of C + +, so initialization is not guaranteed. Using STL part of C + +, such as vector, will ensure initialization.

The best way to do this is to always initialize an object before using it. For built-in types, such as int and pointer, initialization must be completed manually.

Initializes a member variable using the member initialization column

// Version initialized based on member initial column 
ABEntry:ABEntry (const std:string& name, const std::string& address, const std::list<PhoneNumber>& phones) 
: theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0) 
{} 
 
// Assignment based version 
ABEntry:ABEntry (const std:string& name, const std::string& address, const std::list<PhoneNumber>& phones) 
{ 
    theName = name; 
    theAddress = address; 
    thePhones = phones; 
    numTimesConsulted = 0; 
} 
 

Pay attention to the difference between assignment and initialization. The version based on assignment actually calls default constructor to assign an initial value, and then uses parameters to assign new values. In addition, temporary objects will be generated, which is inefficient. In fact, based on the assignment version, the default constructor is used first, and then the copy assignment is used. The initial column of the member only uses the copy constructor. For const and reference, they have only initial values and cannot be assigned.

Note that all member variables should always be listed in the member initial value column. Of course, there are many member variables. Assignment operation can be reasonably adopted, and the assignment operation can be moved to a function (usually private) for all constructors to call. However, compared with the "pseudo initialization" completed by assignment operation, the "real initialization" completed by the member initial column is often preferable.

In addition, the initial columns of members should preferably be in the order of declaration.

Define the initialization order of non local static objects in different compilation units

Above: explicitly initialize the built-in member variables and ensure that the constructor initializes base classes and member variables with the member initial value column, so that only one thing remains to worry about. Define the initialization order of non local static objects in different compilation units.

Static objects have a lifetime from construction to the end of the program, so static and heap based objects are excluded. These objects include global objects, objects defined in the namespace scope, objects in class es, functions, and objects declared as static in the scope. Static objects in functions are called local static objects, and other static objects are called non local static objects. Program structure The static object is automatically destroyed when binding, that is, the destructor will be called automatically at the end of main().

translation unit refers to the source code that produces a single object file. It is basically a single source file plus an included header file. Often, each cpp file is a compilation unit. The compiler will not compile. h or. hpp files

The real problem is that if the initialization of a non local static object in a compilation unit uses a non local static object in another compilation unit, the object it uses may not be initialized.

For example, suppose a FileSystem class, which needs to generate a special object in the global or namespace scope for customers to use

class FileSystem { 
public: 
    std::size_t numDisks() const;  // One of many member functions 
    ... 
} 
extern fileSystem tfs;  // Prepared for customer use 

Obviously, when customers use tfs, there is no guarantee that tfs has been initialized. You can't even find the order after using the template. The solution is to move the non local static object into its function, and the object is declared as static in the function. These functions return a reference to the contained object, and the user calls these functions. C + + guarantees that the local static object in the function will be initialized for the first time when the function is called. Therefore, the initialization guarantee is obtained by calling these functions.

class FileSystem {...}; 
FileSystem& tfs() 
{ 
    static FileSystem fs; 
    return fs; 
} 
 
class Directory {...} 
{ 
    std::size_t disks = tfs().numDisks();    // Call the tfs() function to get a reference to the object, not the object itself 
} 

The above reference returning will have A good effect on A single thread, but object A must be initialized before object B, and the initialization of A is not subject to the initialization of B. This is necessary to avoid premature use before object initialization

  1. Manually initialize the built-in non member object.
  2. Use the member initialization lists column to process all components of the object. Try not to use assignment operations inside constructors.
  3. Strengthen the design under the uncertainty of initialization order. In the cross compilation initialization order problem, replace the non local static object with the local static object.

2, Construction / deconstruction / assignment operation

2.1 clause 05: understand which functions C + + silently writes and calls

Generally, for empty classes, the C + + compiler declares a copy constructor, a copy assignment operator and a destructor for the compiler version. If no constructor is declared, the compiler also declares a default constructor. These functions are public and inline. (explore the C + + object model and say that the compiler should add the above functions according to conditions)

class Empty { }; 
 
//  Equivalent to writing the following code 
class Empty { 
public: 
    Empty() { ... } 
    Empty(const Empty& rhs) { ... } 
    ~Empty() { ... } 
 
    Empty& operator= (const Empty& rhs) { ... } 
}; 

Only when these functions are needed and called will they be created by the compiler. For example

Empty e1;    // default constructor 
Empty e2(e1);    // copy constructor 
e2 = e1;    // Copy assignment operator 
  • The destructor generated by the compiler is non virtual, unless the base class of this class declares a virtual destructor
  • Copy constructor and copy assignment operator. The version created by the compiler simply copies each non static member variable of the source object to the target object.
template <typename T> 
class NamedObject { 
public: 
    NamedObject (const char* name, const T& value); 
    NamedObject (const std::string& name, const T& value); 
    ... 
private: 
    std::string nameValue; 
    T objectValue; 
}; 

The above NameObject declares a constructor, and the compiler will no longer create a default constructor for it. This is very important because if you design a class and the constructor requires arguments, you don't have to worry that the compiler will add a parameterless constructor (i.e. default constructor) to cover your version.

NamedObject does not declare copy constructors and assignment operators. If they are called, the compiler will create those functions for it. Take copy constructors as an example

NamedObject<int> no1("Smallest Prime Number",2);    // Constructor 
NamedObject<int> no2(no1);    // copy constructor  

Note that the copy constructor sets no2.nameValue and no2.objectValue with the initial values of no1.nameValue and no1.objectValue. Among them, nameValue is a string, and the standard string has a copy constructor. Therefore, the method for setting the initial value of nameValue is to call the copy function of string. The objectValue is embodied as int type, which is a built-in type. Therefore, no2.objectValue will copy no1.objectValue E to complete initialization.

The copy assignment behavior is similar to the copy constructor. However, note that the compiler will generate a copy assignment only if the behavior is legal during assignment. For example

template <typename T> 
class NamedObject { 
public: 
    NamedObject (const char* name, const T& value); 
    NamedObject (const std::string& name, const T& value); 
    ... 
private: 
    std::string& nameValue;    // Now it's a reference 
    const T objectValue;    // Now it's a const 
}; 
 
std::string newDog("Persephone"); 
std::string oldDog("Satch"); 
NamedObject<int> p(newDog, 2); 
NamedObject<int> s(oldDog, 36); 
 
p = s;    // Will report an error 

As mentioned above, because nameValue is a reference and objectValue is a const, that is, the reference itself cannot be changed and cannot be assigned (only initial value). The meaning of copy constructor is to set the initial value, and copy assignment is to assign value.

In the face of the above problems, the response of C + + is to refuse to compile the assignment operation. In other cases, when the copy assignment of base class is set to private, the compiler will refuse to generate the copy assignment operator for derived class. Obviously, the reason is that the compiler cannot allow derived class to call the private member of base class.

2.2 clause 06: if you do not want to use the function automatically generated by the compiler, you should explicitly reject it

Sometimes, we need to prevent cpoy construction and copy assignment to ensure that the object is unique and cannot create a copy of the object.

class HomeForSale { ... } 
HomeForSale h1; 
HomeForSale h2; 
HomeForSale h3(h1);    // Attempt to copy h1, should not compile 
h1 = h2;    // Attempts to copy h2 should not be compiled 

The problem is that if the copy constructor and copy assignment operator are not declared, the compiler may generate a copy. The solution is to declare the copy constructor and copy assignment as private. By declaring member functions, the compiler is prevented from secretly creating a proprietary version and making the function private, so as to prevent external calls.

Generally, the above method is not absolutely safe, because the member function and friend function can still be called. At this time, you can define the member function as private but do not implement them.

class HomeForSale { 
public: 
    ... 
private: 
    ... 
    HomeForSale( const HomeForSale& );    // Only the declaration is required, and the parameter name is not required, because it is not intended to be used 
    HomeForSale& operator=( const HomeForSale& ); 
}; 

With the above definition, when trying to copy HomeForSale objects, the compiler will block them. If you do so in the member or friend function, it's the linker's turn to complain (because only the declaration has no definition).

Move the connection time error to the compilation time, which can be implemented using the inheritance structure. Set the copy constructor to private and inherit it: the member and friend functions in the inheritance structure cannot call private members

class Uncopyable { 
protected:    // Allowable construction and Deconstruction 
    Uncopyable() {} 
    ~Uncopyable() {} 
} 
private:    // Block copy 
    Uncopyable( const Uncopyable&); 
    Uncopyable& operator= (const Uncopyable&); 
}; 
 
class HomeForSale: private Uncopyable { 
    ...// The copy constructor and copy assignment operator are no longer declared 
}; 

Based on the above inheritance structure, HomeForSale cannot call copy construction and copy assignment, including member and friend functions.

2.3 clause 07: declare a virtual destructor for a polymorphic base class

C + + polymorphism refers to dynamic binding. There are two conditions:

  1. virtual function exists
  2. Returns the base class pointer to the derived class object
class TimeKeeper { 
public: 
    TimeKeeper(); 
    ~TimeKeeper(); 
    ... 
}; 
 
class AtomicClock: public TimeKeeper { ... };    // atomic clock 
class WaterClock: public TimeKeeper { ... };    // water clock 
class WristWatch: public TimeKeeper { ... };    // Wristwatch 
 
TimeKeeper* getTimeKeeper();    // Returns a pointer to a derived class object of TimeKeeper 

In the inheritance structure, customers often only need to care about TimeKeeper, not specific types (subclasses). Factory functions can be designed to return a pointer to the dynamic allocation object of TimeKeeper derived classes.

At this time, there is a problem in delete, that is, the derived object needs to be deleted through the base pointer. In this case, you need to set the virtual destructor for the base class to ensure that the base object and the derived object are destroyed at the same time.

Note that it is necessary to set the destructor to virtual only when C + + tries to be polymorphic. If the class does not contain the virtual function, making the destructor to virtual will increase the object volume in vain.

Similarly, try not to inherit the class of a non virtual destructor, which may cause problems

class SpecialString: public std::string {... }; // Note that the string destructor is non virtual 
 
// If the above situation inherits the non virtual destructor, the following code may have problems 
SpecialString* pss = new SpecialString("Impending Doom"); 
std::string* ps; 
ps = pss;    // SpecialString* => std::string* 
delete ps;    // In fact, the SpecialString destructor was not called 

C + + does not provide a mechanism similar to Java final or C# sealed to prohibit derivation. Similarly, the analysis uses standard containers such as STL such as vector. Note, however, that base class is not designed for polymorphic purposes, such as standard string and STL. This design does not consider polymorphic inheritance, so there is no need to define virtual destructors.

To sum up:

  1. A polymorphic base class should declare a virtual destructor. If class contains a virtual function, it should have a virtual destructor
  2. Class is not designed to be used as a base class, or polymorphism is not considered. You shouldn't declare a virtual destructor

2.4 clause 08: don't let exceptions escape the destructor

The above reasons are very simple. Exceptions thrown during the destruct process may easily lead to ambiguous behavior, and the object may not be destructed successfully. commonly

For example, database connection class

class DBConnection { 
public: 
    ... 
    static DBConnection create(); 
    void close(); 
    ~DBConn() 
    { 
        db.close(); 
    } 
private: 
    DBConnection db; 
}; 

To ensure that customers do not forget to call close on DBConnection objects, a reasonable idea is to create a class for managing DBConnection resources and call close in its destructor.

The above code call may cause an exception, and the DBConn destructor will propagate the exception, which may throw an exception.

Redesign the DBConn interface to give its customers the opportunity to respond to possible problems.

class DBConn { 
public: 
    void close() 
    { 
       db.close(); 
       closed = true; 
    } 
    ~DBConn() 
    { 
        if (!closed) { 
            try { 
                db.close(); 
            } 
            catch (...) { 
                // Record call to close failed 
            } 
        } 
     } 
private: 
    DBConnection db; 
    bool closed; 
} 

Move the close call from the DBConn destructor to the DBConn client (double insurance call in the destructor). Even if an exception occurs, it cannot be thrown from the destructor, because an exception in the destructor will always bring risks.

Functions that may cause exceptions, such as database connection, file connection, network connection, etc., should not be placed in the destructor, but provide a common function to perform the operation.

2.5 clause 09: never call the virtual function in the process of construction and analysis.

As mentioned in the above terms, when C + + polymorphic structure is required, the destructor needs to be set to virtual. This clause is that virtual functions should not be called in constructors and destructors. The method of calling virtual function is not wrong, but it will not bring polymorphism effect, which increases the complexity in vain compared with ordinary functions.

When the constructor of derived class calls virtual, the content of base class will not be polymorphic. Because the base class is constructed before the derived class, the virtual function will never drop to the derived class layer during the base class construction. In other words, during the construction of base class, the virtual function is not a real virtual function.

Similarly, for the destructor, when it reaches the base class, the derived class destructor process has already been executed. The virtual function is actually equivalent to the ordinary function of the base class.

During construction, you cannot use the virtual function to call down from the base class, but you can pass the necessary construction information up to the base class constructor through the derived class.

class Transaction { 
public: 
    explicit Transaction(const std::string& logInfo); 
    void logTransaction(const std::string& logInfo) const; // Set to non virtual 
}; 
Transaction::Transaction(const std::string& logoInfo) 
{ 
    logTransaction(logInfo); 
} 
class BuyTransaction: public Transaction { 
public: 
    // Pass the log information to the base class constructor 
    BuyTransaction( parameters) : Transaction(createLogString( parameters )) { ... } 
private: 
    static std::string createLogString( parameters ); 
}; 

2.6 Clause 10: make operator = return a reference to *this

Note that this is only a protocol and is not mandatory. If you do not comply with the code, you can compile it. Related operations including + =, = should follow the Convention of returning * this

class Widget { 
public: 
    Widget& operator=(const Widget& rhs) 
    {     
        return *this; 
    } 
 
    Widget& operator+=(const Widget& rhs) 
    {     
        return *this; 
    } 
}; 

2.7 Clause 11: Handling self assignment in operator =

Self assignment occurs when an object is assigned to itself

class Widget { ... }; 
Widget w; 
... 
w = w;    // Assign to yourself 
a[i] = a[j]; //It is possible to assign values to yourself 
*px = *py; // If PX and py point to something, it is also self assigned 

In fact, if a piece of code operates pointer or reference to point to multiple objects of the same type, you need to consider whether these objects are the same. If two objects come from the same inheritance system, they may cause aliases without even declaring the same type.

Widget& Widget::operator=(const Widget& rhs) 
{ 
    delete pb;    // It is possible to delete rhs at the same time 
    pb = new Bitmap(*rhs.pb); 
    return *this; 
} 

It is possible that * this and rhs are the same object, so delete pb also deletes the rhs object.

Traditional solutions

Widget& Widget::operator=(const Widget& rhs) 
{ 
    if (this == &rhs) return *this;    // If it's self assigned, don't do anything 
    delete pb; 
    pb = new Bitmap(*rhs.pb); 
    return *this; 
}

The problem is that if the new Bitmap causes an exception, the pointer pb still points to a deleted Bitmap, which is harmful.

Widget& Widget::operator=(const Widget& rhs) 
{ 
    Bitmap* pOrig = pb;    // Remember the original pb 
    pb = new Bitmap(*rhs.pb);     
    delete pOrign;    // New successfully creates a new Bitmap and deletes the old one 
    return *this; 
} 
// Using copy and swap technology 
Widget& Widget::operator= (const Widget& rhs) 
{ 
    Widget temp(rhs);    // Make a copy for rhs 
    swap(temp);    // Exchange * this data with the data of the above copy 
    return *this; 
} 

Now, if the new Bitmap is abnormal, pOrig will remain unchanged.

  1. Make sure that operator = has good behavior when the object is self assigned, including object address, statement order, copy and swap
  2. Ensure that any function still behaves correctly if it operates on more than one object, and multiple of them are the same object.

2.8 Clause 12: do not forget every component when copying an object

A well-designed object-oriented system will encapsulate the interior of the object, leaving only two functions responsible for object copying, namely copy constructor and copy assignment. Article 5 observes that the compiler creates a copying function for class when necessary, and makes a copy of all member variables of the copied object.

If you declare your own copy function, it is equivalent to telling the compiler that the default implementation is not required. At this time, the compiler often does not give a prompt when the code makes an error. Often at this time, the compiler will not remind when the variable copy is incomplete.

// In the first case, variable replication is incomplete 
class Date { ... }; 
class Customer { 
public: 
    ... 
private: 
    std::string name; 
    Date lastTransaction; 
}; 
Customer& Customer::operator=(const Customer& rhs) 
{ 
    name = rhs.name;    // Date lastTransaction is not copied 
    return *this; 
} 

Note that the components of the above copied objects are incomplete, and the compiler is unlikely to remind you. But once inheritance occurs, it will trigger a crisis

class PriorityCustomer : public Customer { 
public: 
    PriorityCustomer(const PriorityCustomer& rhs); 
    PriorityCustomer& operator= (const PriorityCustomer& rhs); 
private: 
    int priority; 
}; 
PriorityCustomer& 
PriorityCustomer::operator=(const PriorityCustomer& rhs) 
{ 
    priority = rhs.priority; 
    return *this; 
} 

It seems that the copying function of PriorityCustomer copies everything of PriorityCustomer, but note that it inherits Customer, but these member variables are not copied. In fact, the Customer component of the PriorityCustomer object is initialized by the Customer default constructor without arguments. The default constructor will perform the default initialization action on name and lastTransaction, and will not assign values with rhs.

Therefore, when writing the copying function for derived class, you need to carefully copy its base class part. Those components are often private and cannot be accessed directly. The copying function of derived class should call the corresponding base class function

PriorityCustomer& 
PriorityCustomer::operator=(const PriorityCustomer& rhs) 
{ 
    Customer::operator=(rhs);    // Assign a value to the base class component 
    priority = rhs.priority; 
    return *this; 
} 

When writing a copying function, make sure

  1. Copy all local member variables
  2. Call the appropriate copying function within all base class es

3, Resource Management

The so-called resources, once used, must be returned to the system in the future. If not, bad things will happen. The most common resource used by C + + programs is dynamic allocation of memory (if not returned, it will lead to memory leakage). In addition to memory, other common resources include file descriptors, mutex locks, database links, font and brush of graphical interface, and network sockets. No matter which resource is no longer used, it is important to return it to the system.

This part of the object-based memory management method is based on C + + to constructor, destructor and copying function.

3.1 Clause 13: manage resources by objects

Suppose you use a program library for investment behavior (such as stocks and bonds), and various investments inherit from a root class Investment

class Investment { ... }; 
// The factory function returns a pointer to the dynamic allocation object in the Investment inheritance system 
Investment* createInvestment();  
 
void f() 
{ 
    Investment* pInv = createInvestment(); 
    delete pInv; 
} 
 
void f() 
{ 
    std::auto_ptr<Investment> pInv(createInvestment()); 
} 
 
std:auto_ptr<Investment> pInv2(pInv1);    // Now pInv2 points to the object and pInv1 becomes NULL 
pInv1 = pInv2;    // pInv1 points to the object and pInv2 becomes NULL 
 
std::auto_ptr<int> spi(new int[1024]);// Use smart pointers on the array. Pay attention 

There is a problem with manual destruct using delete. f may not be able to delete the object, and f may not always be able to delete, and the impact of errors is huge. In order to ensure that the resources returned by createInvestment are always released, it is necessary to put the resources into the object. When the control flow leaves f, the destructor of the object will automatically release those resources without calling delete.

The auto_ptr provided by the standard library is a class pointer object, that is, the so-called "smart pointer". The destructor of the auto_ptr object automatically calls delete on the object.

Two key ideas of managing resources with objects

  1. After obtaining the Resource, it is immediately put into the management object. The Resource returned by createInvestment above is regarded as the initial value of the manager's auto_ptr. It is also called "Resource acquisition time is initialization time" (Resource)
    Acquisition Is Initialization RAII)
  2. The management object uses a destructor to ensure that resources are released. Obviously, the management object is created on the stack, and the destructor is called automatically after the control flow leaves.

Since the object is automatically deleted when auto_ptr is destroyed, be sure not to let multiple auto_ptrs point to the same object. If the object is deleted more than once, the program will drive to "undefined behavior".

To prevent this problem, auto_ptr has the following properties:

If copy constructor or copyassignment is used, the original auto_ptr is assigned NULL, and the assigned object points to the object. Due to the above characteristics, no more than one auto_ptr points to the resources managed by auto_ptr at the same time. This sometimes fails to meet the requirements. For example, STL containers need normal replication behavior, and these containers cannot accommodate auto_ptr.

An alternative to auto_ptr is the reference counting smart pointer (RCSP). RCSP is also a smart pointer that continuously tracks how many objects point to a resource and automatically deletes the resource when no one points to it. In fact, it is similar to garbage collection. shared_ptr is actually this kind, which can realize multiple pointers pointing to the same object.

Note that both auto_ptr and share_ptr do delete in the destructor instead of delete [], which means that it is a bad idea to use auto_ptr or share_ptr on dynamically allocated array s.

Sometimes these preset classes cannot be managed properly. In this case, it is necessary to make resource management classes skillfully. To sum up

  1. To prevent resource leakage, use RAII objects, which get resources in the constructor and release resources in the destructor
  2. The two commonly used raii classes are share_ptr and auto_ptr. The former is usually the better choice because the auto_ptr copy action will make the copied object point to NULL.

3.2 Clause 14: beware of copying in resource management

"Resource acquisition time is initialization time" in Clause 13 describes that auto_ptr and share_ptr are used on heap based resources. However, not all resources are heap based, and sometimes it is necessary to establish their own resource management classes.

Suppose there are mutex objects of Mutex type, and lock and unlock functions are available. Make sure you don't forget to unlock the locked Mutex and establish a class management lock. Such a basic class structure is governed by the RAII code, that is, resources are obtained during construction and released during destruction.

class Lock { 
public: 
    explicit Lock(Mutex* pm) : mutexPtr(pm){ 
        lock(mutexPtr);    // Display transformation to obtain resources 
    } 
    ~Lock() { unlock(mutexPtr); }    // Release resources 
} 
private: 
    Mutex* mutexPtr; 
}; 
 
Mutex m;    // Define mutex 
{ 
    Lock m1(&m);    // Obtain resources during construction and lock mutexes, which conforms to RAII mode 
}    // At the end of the block, call the destructor to automatically release resources and unlock the mutex 
Lock m2(m1);    // Copy m1 to m2. What happens?? 

But the question is what happens when a RAII is copied. There are generally two possibilities

  1. Prohibit copying. Many times, it is unreasonable for RAII to be copied. For example, locks generally do not have multiple objects to obtain resources. For example, auto_ptr
  2. The reference counting method of the underlying resource. That is, destroy the object until the last user. Copy the object and increase the internal reference counter of the object. For example, share_ptr.

Note that when copying a resource management object, you should copy its wrapped resources at the same time, that is, deep copy. Often, when it contains a pointer and a pointer to a heap memory, both the pointer and the memory will be made into a copy, that is, deep copy behavior.

3.3 clause 15: provide access to original resources in resource management class

Although the resource management class can manage resources well and eliminate resource leakage, sometimes the API only involves resources, and the resource management class is required to provide access to package resources.

std::shared_ptr<Investment> pInv(createInvestment()); 
// You want a function to handle the Investment object 
int dayHeld(const Investment* pi);    // Return investment days 
 
int days = daysHeld(pInv);    //error 
int days = daysHeld(pInv.get());    // ok 

The above call cannot be compiled because daysHeld needs the Investment * pointer, but the shared_ptr object is passed in. At this time, a function is required to convert the RAII class object (shared_ptr above) into its original resource (Investment *) above. There are two methods, explicit conversion and implicit conversion.

Both shared_ptr and auto_ptr overload pointer value operators (Operator - > and operator *), which allow implicit conversion to the original pointer. At the same time, a get member function is provided to perform explicit conversion, that is, to return the original internal pointer.

If it is a user-defined resource management class, you can write the display conversion in this way

class Font { 
public: 
    explicit Font(FontHandle fh) :f(fh) {}; 
    ~Font() { relaseFont(f); } // Release resources 
 
    FontHandle get() const { return f; }    // Explicit conversion function 
private: 
    FontHandle f;    // Original resources 
} 

Above, the resource management class Font can obtain the internal original resources by calling get().

3.4 Clause 16: use new and delete in pairs in the same form

In short:

If [] is used in new, it must also be used in delete expression, and vice versa.

delete [] assumes that the pointer only wants an array of objects. Be careful not to use typedef action on array form.

typedef std::string AddressLines[4];    // Come in don't do that 
std::string* pal = new AddressLines;    // Equivalent to new string[4] 
delete [] pal;    // Need to match delete [] 

The above AddressLines can be defined as vector as much as possible

3.5 Clause 17: put the new object into the smart pointer with a separate statement

Suppose we have a function to reveal the priority of the handler and another function to process it

int priority(); 
void processWidget(std::shared_ptr<Widget> pw, int priority); 
 
processWidget(new Widget, priority());    // Cannot compile 
processWidget(std::shared_ptr<Widget>(new Widget), priority()); //Possible leakage of resources 

The first call above cannot be compiled, shared_ The PTR constructor requires a raw pointer, but the constructor is an explicit constructor and cannot be implicitly converted.

However, although the second one can be compiled, it may leak resources.

std::shared_ptr(new Widget) consists of two parts

  1. Execute new Widget
  2. Call std::shared_ptr constructor

priority() is also executed. The problem is that it is possible to execute the new Widget successfully, but std::shared_ptr construction failed. The return pointer of the new Widget is lost, causing resource leakage.

The way to avoid this problem is simple, using separate statements

std::shared_ptr<widget> pw(new Widget); 
processWidget(pw, priority()); 

The above also shows that the actual parameters passed by the function should not be too complex.

4, Design and declaration

This part is mainly the design and declaration of C + + interface. That is, "make the interface easy to be used correctly and not easy to be misused". Interfaces are divided into class, function and template.

4.1 Clause 18 makes the interface easy to be used correctly and not easy to be misused

C++ function interface, class interface and template interface. Each interface is a means for customers to interact with code. Ideally, if a customer attempts to use an interface and does not get the expected behavior, the code should not be compiled. To develop an interface that is easy to use correctly and not easy to be misused, we must first consider what kind of errors customers may make. For example

class Date { 
public: 
    Date(int month, int day, int year); 
}; 

The restrictions of Date and month should be considered. For example, Date(2,30,1995) cannot be compiled. You can import simple wrapper types to distinguish days, months, and years, and then use these types in the Date constructor.

struct Day { 
    explicit Day(int d) : val(d) { }     
    int val; 
}; 
struct Month { 
    explicit Month(int m) : val(m) { } 
    int val; 
}; 
struct Year { 
    explicit Year(int y) : val(y) { } 
    int val; 
}; 
 
class Date { 
public: 
    Date(const Month& m, const Day& d, const Year& y); 
    ... 
}; 
 
Date d(Month(3), Day(30), Year(1995));    // Correct type  

However, compared with struct, it is better to use class and encapsulate the data in it. At the same time, its value is limited. For example, there are only 12 months in a year. One way is to use enum to represent months, but enum is not encapsulated. The more effective method is to define all valid months in advance

class Month { 
public: 
    static Month Jan() { return Month(1); } 
    static Month Feb() { return Month(2); } 
    ... 
    static Month Dec() { return Month(12); } 
private: 
    explicit Month(int m);    // Block outside access 
}; 
Date d(Month::Mar(), Day(30), Year(1995)); 
 

"Replace the object with a function to represent a specific month". The reason is that there may be a problem with the initialization of the non local static object. Read clause 4 to restore the memory.

For example, Clause 13 imports a factory function, which returns a pointer to a dynamic allocation object in the Investment inheritance system

Investment* createInvestment(); 
 
std::shared_ptr<Investment> createInvestment();  // Direct return smart pointer 
 
std::shared_ptr<investment> pInv(static_cast<Investment*>(0), 
getRidOfInvestment);    // Initialize to convert null into a pointer of Investment type 

To avoid resource leakage, when the pointer returned by createInvestment is deleted, at least two customer errors should be enabled: the pointer is not deleted, or the same pointer is deleted more than once.

Similarly, in order to prevent the client from forgetting to use the smart pointer, the smart pointer is returned directly. A constructor of shared_ptr accepts two arguments, one is the managed pointer and the other is the delegator called when the number of references is 0.

The shared_ptr of Boost is twice as large as the original pointer. The delegator is called in the form of virtual, and the number of references are modified by multiple threads. Obviously, it is larger and slower than the original pointer, but the effect of reducing user errors is indeed remarkable.

To sum up

  1. Methods to promote proper use include interface consistency and compatibility with the behavior of built-in types
  2. Ways to prevent misuse include creating new types, limiting operations on types, constraining object values, and eliminating customer resource management responsibilities

4.2 Clause 19: design type

C + + is like other OOP (object-oriented programming) languages. When defining a new class, it defines a new type. Overload functions and operators, control the allocation and return of memory, and define the initialization and termination of objects... It's all in your hands. Designing efficient classes first faces the problem

  1. How new type objects are created and destroyed affects the design of class constructors and destructors, as well as memory allocation and release functions (operator new,operator new[], operator delete, operator delete [])
  2. What is the difference between object initialization and object assignment? This determines the behavior of constructors, copy constructors and assignment assignment operators. Be careful not to confuse initialization and assignment
  3. type object what it means if the passed by value value value is passed. The copy constructor defines the implementation of pass by value
  4. The legal value of the new type. Generally, only some values are valid for class member variables. These constraints also determine the error checking that member functions (especially constructors, assignment operators and setter functions) must perform.
  5. If you integrate some existing classes, you will be bound by the design of those classes, especially virtual.
  6. What kind of conversion is required for type. If you want T1 to be implicitly converted to T2, you must write a type conversion function (operator T2) in class T1 or a non explicit one argument constructor in class T2 that can be called by a single argument. If explicit exists, you must specifically execute the converted function.
  7. What operators and functions are reasonable for type
  8. Who should ask the type member to set the public, protected, private members and the friend function
  9. How generic type is. Define the type family. If you need parameter generalization, you need to define class template
  10. Do you really need type? If you define a new derived class to add functions to the existing class, non member or template can achieve the goal better.

The above questions are not easy to answer, so defining efficient classes is a challenge. However, if you can design user-defined classes like C + + built-in types, all the sweat will be worth it.

Remember that the design of class is the design of type. Please determine all the above problems before defining a new type.

4.3 Clause 20: try to replace pass by value with pass by reference to const

By default, C + + passes objects to functions by value (a method inherited from C). By default, function parameters take a copy of the actual parameter as the initial value, and the caller obtains a copy of the function return value. These copies are generated by the copy constructor of the object, which may make pass by value a costly operation.

Consider examples

class Person { 
public: 
    Person(); 
    virtual ~Person();    // Polymorphic virtual deconstruction 
private: 
    std::string name; 
    std::string address; 
}; 
class Student : public Person { 
public: 
    Student(); 
    ~Student(); 
private: 
    std::string schoolName; 
    std::string schoolAddress; 
}; 
 
// Calling code 
bool validateStudent(Student s);    // Accept parameters as by value 
Student plato; 
bool platoIsOK = validateStudent(plato); 
 
// pass by reference-to-const 
bool validateStudent(const Student& s); 

In fact, there are two string objects in Student, and so is Person. Pass in Student s in the form of by value, and actually call Student copy function once, Person copy function once, and four string copy functions in total. The overall cost of passing values by value is six copy constructors and six destructors.

When using pass by reference const, no constructor or destructor is called because no new object is created. It is necessary to pass the declaration of const by reference, otherwise the object may be modified.

Passing parameters by reference can also avoid the slicing object cutting problem caused by value passing. as follows

class Window { 
public: 
    std::string name() const;    // Return window name 
    virtual void display() const;    // Display window, content 
}; 
class WindowWithScrollBars: public Window { 
public: 
    virtual void display() const; 
}; 
 
// Print function 
void printNameAndDisplay(Window w)    // Parameters may be cut 
{ 
    std::cout << w.name(); 
    w.display(); 
} 
 
 
WindowWithScrollBars wwsb; 
printNameAndDisplay(wwsb); 

The child object WindowWithScrollBars is passed in, but pass by value in the function will only represent a Window object. The specialized information of all WindowWithScrollBars will be deleted, and the derived class will be transformed upward to base class when it reaches the function.

The solution to the cutting problem is to pass in const window & W by reference. At this time, what type is passed in, w shows that type.

Try to replace pass by value with pass by reference to const. The former is usually more efficient. However, this rule does not apply to built-in types, as well as iterators and function objects of STL. Traditionally, the iterator of STL is designed as pass by value. This rule is also Clause 1: the change of the rule depends on which part of C + + is used.

4.4 Clause 21: when an object must be returned, don't try to return its reference

In the firm pursuit of pass by reference purity, you may make a fatal mistake: Returning Reference to an object that does not actually exist. This means that not everything can return reference.

In short, if a function returns a pointer or reference, make sure that the object pointed to by the pointer still exists after the return.

Create objects in stack

const Rational& operator* (const Rational& lhs, const Rational& rhs) 
{ 
    Rational result(lhs.n * rhs.n, lhs.d * rhs.d);    // Bad code 
    return result; 
}

In the above code, result is a local stack object, and the local object is destroyed before the function exits. Therefore, it is dangerous that the returned result is actually "undefined behavior".

Constructing objects in heap

const Rational& operator* (const Rational& lhs, const Rational& rhs) 
{ 
    Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d); 
    return *result; 
} 
 
// The following code can cause a memory leak 
Rational w,x,y,z; 
w = x * y * z;    // Equivalent to operator*(operator(x,y),z) 

The above question is who should delete the new object. In fact, the above code can easily lead to memory leaks. For example, the concatenation operator new two objects, but you cannot use delete on them.

Obviously, whether on the stack or on the heap, the return reference can easily lead to errors. In fact, due to the establishment of objects, the efficiency of Returning References does not need to return objects directly.

The conclusion is that when an object must be returned, don't try to return its reference. When you have to choose between returning a reference and returning an object, your job is to pick the one that behaves correctly. Let compiler manufacturers strive to reduce costs as much as possible. You just need to choose the ones that behave correctly.

4.5 Clause 22: declare member variables as private

  1. Syntax consistency. If the member variable is not public but private or protected, the only way for customers to access the object is through the member function, so as to have syntax consistency.
  2. Through function access, you can realize "no access", "read-only access", "read-write access" and other controls. Subdivision control can avoid setting a getter function and setter function for each member variable.
class AccessLevels { 
public: 
    int getReadOnly() const { return readOnly; } 
    void setReadWrite(int value) { readWrite = value; } 
    int getReadWrite() const { return readWrite; } 
    void setWriteOnly(int value) { writeOnly = value; } 
private: 
    int noAccess;    // Cannot access 
    int readOnly;    // read-only access  
    int readWrite;    // Read write access 
    int writeOnly;    // Write only access 
};  
  • Encapsulation. Accessing a member variable with a function can replace this member variable in the future, and the class customer will not know the changes inside class. public means no encapsulation. Non encapsulation means that it is easy to be destroyed, code dependency is strong, and it is not easy to maintain and update.
  • For a public member variable, you can even cancel it if you make changes. All customer codes using it will be destroyed; For a protected member variable, all derived class es will be destroyed after changing; For private, no customer code will be destroyed. Obviously, using private is easier to maintain. Customers only use objects and don't need to know about internal structure changes.

4.6 clause 23: try to replace the member function with non member and non friend

A class represents a web browser

class WebBrowser { 
public: 
    void clearCache();    // Clear cache 
    void clearHistory();    // Clear history 
    void removeCookies();    // Clear cookies 
 
    // To perform the above three actions at once, use the member function 
    void clearEverything(); 
} 
 
// Use the non member function to call the member function to implement the above three actions 
void clearBrowser(WebBrowser& wb) 
{ 
    wb.clearCache(); 
    wb.clearHistory(); 
    wb.removeCookies(); 
}  

Above, which is better to use the member function clearEverything and the non member function clearBrowser?

Discussion from encapsulation: if it is encapsulated, it will no longer be visible. The more things are encapsulated, the less people can see it, so there is more flexibility to change it. Therefore, encapsulation can be realized to change things and affect only a limited number of customers.

The only functions that can access private member variables are the member function of class plus the friend function. Non member and non friend functions can lead to greater encapsulation because they cannot increase the number of functions that can access private variables in class. Therefore, we should try to use non member function instead of member function.

Generally, the above clearBrowser function can be turned into a static member function of a tool class untity class. As long as it is not part of the WebBrowser or its friend, it will not affect the encapsulation of the private members of the WebBrower. A more natural approach in C + + is to put both in the same namespace

namespace WebBrowserStuff { 
    class WebBrowser { ... }; 
    void clearBrowser(WebBrowser& wb); 
} 

The reason for using namespace is that namespace can span multiple source files, but class cannot. Using namespace can improve extensibility and avoid compilation dependency, as follows

// The header file webbrowser.h is for the class WebBrowser itself 
namespace WebBrowserStuff { 
class WebBrowser { ... };    // Core function 
} 
 
// Header file webbrowserbookmarks.h 
namespace WebBrowserStuff { 
    ...     // Convenience functions related to bookmarks 
} 
 
// Header file webbrowsercookies.h 
namespace WebBrowserStuff { 
    ... // cookie related convenience functions 
} 

Above, put the core WebBrowser, bookmark related convenience functions and cookie related convenience functions in three header files. You can #include whichever you need to reduce the compilation dependency of multiple files.

The three functions are placed in the same namespace, and the convenience functions can be easily extended through the namespace.

In fact, the above is the organization of the C + + standard library. The standard library has a large number of header files, such as, etc., and each header file declares the functions related to the std namespace. When users use List, they only need to include, which forms a compilation dependency on only a small part of the system used. By declaring std, you can easily extend the function of standard library files. The class must be defined as a whole and cannot be cut into fragments, because the class cannot be accessed directly outside the file.

The namespace client can also easily extend these functions. For example, if the WebBrowser client decides to write convenient functions related to image download, it only needs to create a header file in the WebBrowserStuff namespace containing the function life. Class cannot provide this property because class cannot be extended by customers. The derived driven class is also a secondary identity and cannot access the encapsulated components of base class.

4.7 Clause 24: if the parameters of a function may have type conversion, set the function to non member

Making class support implicit conversions is usually a bad idea, but there are exceptions, most commonly numeric types. For example, int is implicitly converted to double. The so-called implicit conversion generally occurs in assignment, copy and initialization, that is, direct conversion without prompting the system for specific types.

Take the operator as an example:

class Rational { 
public: 
    // Constructor is not explicit, int to rational implicit conversion is allowed 
    Rational(int number = 0, int denominator = 1); 
    int numerator() const; 
    int denominator() const; 
private: 
    ... 
};     

Set an operator * and write it as a Rational member function

class Rational { 
public: 
    const Rational operator* (const Rational& rhs) const; 
}; 
 
// test 
Rational oneEighth(1,8); 
Rational oneHalf(1,2); 
Rational result = oneHalf * oneEighth // ok 
result = oneHalf * 2;    // Good, implicit conversion occurs, const Rational temp(2); 
result = 2 * oneHalf;    // error 

Only half of the above hybrid operations work. The reason is that there is no 2.operator*(oneHalf). The former has the implicit conversion int - > rational. If the constructor is set to explicit, none can be compiled.

To solve the above problems, use the non member function to solve the possible implicit conversion problem.

class Rational { 
public: 
    ... 
}; 
const Rational operator* (const Rational& lhs, const Rational& rhs) 
{ 
    return Rational (lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator() ); 
} 
 
result = oneHalf * 2;    //  
result = 2 * oneHalf;    // Can be compiled 

4.8 Clause 25: consider writing a swap function without throwing exceptions

This clause mainly talks about how to write a good swap function. The so-called swap replaces two object values. By default, the swap action can be completed by the swap algorithm provided by the standard library, as follows.

namespace std { 
    template<typename T> 
    void swap(T& a, T& b)    // Replace the values of a and b 
    { 
        T temp(a); 
        a = b; 
        b = temp; 
    } 
} 

The above requires T to support copying (one copy constructor and two copy assignment operators). However, this implementation method wastes too much resources for some types, especially for pimpl.

The so-called pimpl technique (pointer to implementation) means that the pointer points to an object, which contains real data, and the outer layer is like a shell. for example

class WidgetImpl { 
public: 
    ... 
private: 
    int a, b, c; 
    std::vector<double> v; 
}; 
class Widget { 
public: 
    Widget(const Widget& rhs); 
    Widget& operator= (const Widget& rhs) 
    { 
        *pImpl = *(rhs.pImpl); 
    } 
private: 
    WidgetImpl* pImpl;    // Pointer to the real data object WidgetImpl 
}; 

Above, replacing two WidgetImpl objects only needs to exchange pointers internally. But the default swap will copy WidgetImpl and all objects inside the Widget, which is very inefficient. At this time, we generally need to design a new swap function.

Generally, because the internal pointer pImpl is a private member, it can only be accessed by member and friend functions. So let's make the Widget declare a public member function of swap to do real replacement work, and then fully specialize std::swap to call the swap member function.

class Widget { 
public: 
    Widget(const Widget& rhs); 
    Widget& operator= (const Widget& rhs) 
    { 
        *pImpl = *(rhs.pImpl); 
    } 
private: 
    WidgetImpl* pImpl;    // Pointer to the real data object WidgetImpl 
}; 
namespace std { 
    template<>    // Specialization of swap function of std 
    void swap<Widget>(Widget& a, Widget& b) 
    {    a.swap(b);    } 
} 
 
namespace std { 
    template<typename T>    // Error, C + + does not support function specialization of class template 
    void swap<Widget<T>>(Widget<T>& a, Widget<T>& b) 
    {    a.swap(b);    } 
}

Above, both Widget and WidgetImpl are class es. You only need to specialize the swap function in std. When Widget and WidgetImpl are template, specialization cannot be used. Because C + + does not support partial specialization of function to template. The solution is to use overloading

class Widget { 
public: 
    void swap(Widget& other) 
    { 
        using std::swap; 
        swap(pImpl, other.pImpl);  // Replace the pointer with the default swap function 
    } 
}; 
namespace std { 
    template<typename T> 
    void swap (Widget<T>& a, Widget<T>& b) 
    {    a.swap(b);    } 
};

The above overloads the swap function of std namespace. When a Widget is passed in, it will call the swap written above, and others will call the default swap function. Overload instead of template specialization is used here because C + + only allows partial specialization of class template, which is not available on function template.

Generally speaking, there is no problem overloading function template. However, the content of std is completely determined by the C + + Standards Committee, that is, new templates, classes and functions can not be added to std, but customers can fully specialize the templates in std.

Therefore, we declare a non member swap to call member swap, but no longer declare non member swap as a specialized or overloaded version of std::swap. Instead, it is placed in the new namespace WidgetStuff.

namespace WidgetStuff { 
    template<typename T>  // Including swap member function 
    class Widget { ... };   
 
    template<typename T> 
    void swap(Widget<T>& a, Widget<T>& b) 
    {    a.swap(b); 
    } 
};    
 
// call 
template<typename T> 
void doSomething(T& obj1, T& obj2) 
{ 
    using std::swap; 
    swap(obj1, obj2);    // The best swap version is automatically called on the T-object 
} 

Although the swap function is not defined in the std namespace, it does not prevent C + + name lookup rules. That is, when the parameter is widget &, the swap of the namespace WidgetStuff will be called preferentially instead of the default swap. Note, however, that no modifiers should be added before swap, such as std::swap. At this time, C + + will automatically select the most appropriate one to call.

To sum up:

  1. If the default implementation of swap provides acceptable efficiency for your class or class template, you can directly use the default swap.
  2. If the default version of swap is inefficient (which basically means that some pimpl method is used in class or template), then:
  • A public swap member function is provided to replace objects efficiently, and the function cannot throw exceptions
  • Provide a non member swap in the class or template namespace and make it call the above swap member function
  • For class, the non member swap is fully specialized directly; For template, reload non member swap and put it in other namespaces

5, Realize

5.1 clause 26: postpone the occurrence time of variable definition formula as far as possible

As long as a variable is defined and its type has a constructor and destructor, when the program control flow reaches the variable definition, it will have to bear the construction cost; When a variable leaves the scope, it has to bear the cost of deconstruction. This efficiency expenditure should be avoided.

std::string encryptPassword(const std::string& password)
{
    using namespace std;
    string encrypted;    // It's too early to define
    if (password.length() < MinimumPasswordLength){
        throw logic_error("Password is too short");
    }
    ...
    return encrypted;
}

Obviously, if there is logic_error exception is thrown. Encrypted is destructed without being used to pay the cost. Therefore, it is best to extend the encrypted definition after exception handling.

void encrypt(std::string& s);    // Encrypt s
std::string encryptPassword(const std::string& password)
{
    ...    // Above
    std::string encrypted(password);    // Through the copy constructor
    encrypt(encrypted);
    return encrypted;
}

Delay as much as possible. Specifically, delay as much as possible until the initial value argument is given. Use a copy constructor instead of the default constructor + assignment operator whenever possible.

// default constructor + assignment operator.
std::string encrypted;
encrypted = password;

// A copy constructor, high efficiency
std::string encrypted(password);

5.2 clause 27: minimize transformation

As a strongly typed language, the design goal of C + + rules is to ensure that type errors cannot occur. However, transformation cast destroys the type system and may lead to various troubles. The transformation of C/Java/C # these languages is necessary, unavoidable and not too dangerous, but C + + is different. Transformational grammar usually has three different forms, as follows

// C-style transformation operation
(T) expression    // Transform expression to T
// Transformation operation of function style
T(expression)
// The following is the new transformation
const_cast<T>(expression)
dynamic_cast<T> (expression)
reinterpret_cast<T> (expression)
static_cast<T> (expression)

The four new transformations have different purposes - const_cast converts the constancy of the object, that is, const to non const - dynamic_ Cast is mainly used to perform secure downward transformation and only supports polymorphic downward transformation. It is the only action that cannot be performed by the old syntax and the only transformation action that may cost significant running costs- reinterpret_cast performs low-level transformations, and the actual actions and results may depend on the compiler. This transformation is dangerous and can only be used in low-level code- static_cast is used to force implicit conversion, such as converting non const to const object, converting int to double, but cannot convert const to non const.

class Widget {
public:
    explicit Widget(int size);
    ...
};
void doSomeWork(const Widget& w);
// Function style, converting an int to Widget type
doSomeWork(Widget(15));
// C + + style static_cast, convert int to Widget type
doSomeWork(static_cast<Widget>(15));

In fact, any type conversion, whether explicit conversion through transformation operations or implicit conversion through the compiler, will make the compiler compile some code executed at run time. Minimize transformation operations and replace transformation with other methods, especially downward transformation dynamic_cast. If transformation is necessary, hide the transformation behind a function. Customers can then call this function instead of putting the transformation into their code.

5.3 Clause 28: avoid returning Handles to point to the internal components of the object

The so-called Handles are used to obtain an object. Pointers, references and iterators are Handles. Returning the Handle of an object's internal component has the risk of reducing the encapsulation of the object, and may even cause malicious modification of private variables.

class Point {    // class representing point
public:
    Point(int x, int y);
    void setX(int newVal);
    int setY(int newVal);
};
strcut RectData {
    Point ulhc;  // Two points can represent a rectangle
    Point lrhc;
};
class Rectangle {
public:
    Point& upperLeft() const { return pData->ulhc; }
    Point& lowerRight() const { return pData->lrhc; }
};
private:
    std::shared_ptr<RectData> pData;
};

Point coord1(0, 0);
Point coord2(100, 100);
const Rectangle rec(coord1, coord2);
rec.upperLeft().setX(50);    // Modified private member

In the above code, although the two points that make up the rectangle are private members, the public member function returns the reference of the private member. That is, the reference points to private data, and the caller can change the internal data through these references. The encapsulation of a member variable may be equal to the access level of its reference function. In this example, although ulhc is declared as private, it is actually public because the public function passes out their reference. We must pay attention to the handles that member functions return internal data. Generally, we add const

class Rectangle {
public:
    const Point& upperLeft() const { return pData->ulhc; }
    const Point& lowerRight() const { return pData->lrhc; }
};
private:
    std::shared_ptr<RectData> pData;
};

By returning const &, the customer can read the rectangular Point, but cannot modify it. However, this situation may still lead to the nonexistence of the object referred to in handles. See article ji paragraph 21: when the object must be returned, don't try to return its reference. Above: avoid returning handles (including reference, pointer and iterator) to Point to the inside of the object.

5.4 clause 29: it is worthwhile to work for abnormal safety

Suppose a class is used to represent the GUI menu, which is used in a multithreaded environment, and there is a mutex as concurrency control.

class PrettyMenu {
public:
    void changeBackground(std::istream& imgSrc);  // Change background image
private:
    Mutex mutex;    // mutex 
    Image* bgImage;    // Current background image
    int imageChanges;    // The number of times the background image has been changed
};

// A possible implementation of the changeBackground function
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    lock(&mutex);    // Get mutex
    delete bgImage;    // Remove old background
    ++imageChanges;
    bgImage = new Image(imgSrc);    //Install new background
    unlock(&mutex);    // Release mutex
}

The above code is very bad. There are two conditions for exception safety, and the function does not meet any of them

  1. Do not disclose any resources. The above code does not do so. Once new Image causes an exception, unlock will not be called, and the mutex will be controlled forever.
  2. Data corruption is not allowed. If new Image throws an exception, bgImage points to the deleted object, and imageChanges are also accumulated, which is unreasonable. To solve the problem of resource leakage, Clause 13 has discussed object management resources. If the object is built on the stack, the destructor will be called automatically to release resources after the control flow leaves the code segment.

The exception safety function provides the following three guarantees:

  1. Basic guarantee. If an exception is thrown, everything in the program will remain in a valid state, and no objects and data structures will be corrupted. However, the actual state of the program may be unpredictable. For example, when the above code exception is thrown, the PrettyMenu object can continue to maintain the original background image or become the default background image.
  2. Strong guarantee. If an exception is thrown, the program state does not change. That is, if the function succeeds, it is completely successful; if it fails, the program returns to the state before calling the function.
  3. Do not throw the nothrow guarantee, promise not to throw exceptions, and always complete the originally promised functions. All operations acting on built-in types provide the nothrow guarantee, which is a key basic material for exception security codes. However, this guarantee is unrealistic for most functions.

Exception security must provide one of the above three guarantees, otherwise it will not have exception security. Obviously, nothrow guarantee is difficult, and most function choices are often between basic guarantee and strong guarantee. For changeBackground, it is not difficult to provide strong guarantee. First, change the bgImage member variable from a built-in pointer of type Image * to an intelligent pointer for resource management imageChanges then arranges the number of statements so that the Image is updated and then accumulated

class PrettyMenu {
    std::shared_ptr<Image> bgImage;
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    Lock m1(&mutex);    // Managing resources with objects
    bgImage.reset(new Image(imgSrc);    // Intelligent pointer maintenance
    ++imageChanges;
}

However, the above deficiency is the parameter imgSrc. If the Image constructor throws an exception, changeBackground can only provide basic exception security before solving this problem. The copy and swap strategy can strongly ensure exceptions. The principle is to make a copy of the object you intend to modify, and then make all necessary modifications on the copy. If the modification is successful, the copy and the original pair If the modification fails, the original object remains unchanged. For PrettyMenu, copy and swap are typically written as follows

struct PMImpl {
    std::shared_ptr<Image> bgImage;
    int imageChanges;
};
class PrettyMenu {
private:
    Mutex mutex;
    std::shared_ptr<PMImpl> pImpl;
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
    using std::swap;
    Lock m1(&mutex);
    std::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl));    // Get a copy
    pNew->bgImage.reset(new Image(imageSrc));    // Modified copy
    ++pNew->imageChanges;

    swap(pImpl, pNew);    // Displacement data
}

Make PMImpl struct because its encapsulation has been guaranteed by making pImpl private. However, due to the joint influence, copy and swap can not guarantee the strong security of the whole function

void someFunc()
{
    f1();
    f2();
}

Obviously, if an exception occurs when f2() performs an operation, it is actually difficult to restore the original state, because there is the execution of f1(). Moreover, if f1() A database has been changed because other customers of the database may have seen the data, which is difficult to restore to the original state. And copy and swap needs to spend time and space on a copy. When strong assurance is impractical, you must provide basic assurance of exception. Note: if a function in the system does not have exception security, the whole system does not have exception security Integrity. A software system either has or does not have exception security. In fact, many old C + + codes do not have exception security, so today many systems still can not be said to be exception security.

5.5 Clause 30: thoroughly understand the inside and outside of inlining

The inline function looks like a function and acts like a function, which is much better than a macro. Calling them does not incur the additional overhead incurred by function calls. Moreover, compiler optimization mechanisms are usually designed to concentrate code that "does not contain function calls", and the inline function compiler can perform better context dependent optimization. Inline will increase the size of the object code, and the program size is too large. This is the disadvantage of the inline function. Inline is only an application to the compiler, not a mandatory command. It can be pointed out metaphorically or explicitly. The metaphorical way is to define functions in class definitions.

// Metaphorical inline, class defined function
class Person {
public:
    int age() const { return theAge; }
private:
    int theAge;
}
// Explicit inline
template<typename T>
inline const T& std::max(const T& a, const T& b)
{ return a < b ? b : a; }

Yes, the function in the class definition is the inline function. The other group is naturally the keyword inline. The inline function is usually placed in the header file because most build environments are inline during compilation. Template is usually also placed in the header file, because once it is called, the compiler needs to know what it looks like in order to materialize. But the materialization of template has nothing to do with inlining. Inline is an application. Most compilers refuse to inline functions that are too complex (such as with loops and recursion). At the same time, virtual functions will not be inline, because virtual means that the runtime determines which function to call. Inline is the compile time behavior. Generally, the compiler gives a warning message after rejecting inline. If you use a function pointer to get the address of an inline function, the inline function will not be inline. The reason is very simple. The function after inline does not exist independently. Naturally, the address cannot be obtained

inline void f() { ... } 
void (*pf)() = f;    // The function pointer pf points to f
f();    //This call will be called normally by Inline
pf();    // This call is not inline because it is achieved by taking the address through the function pointer

Constructors and destructors should not be set to inline, because compilers often add a lot of content to them. In fact, constructors and destructors are functions with huge content. To sum up, inline should be limited to small and frequently called functions, which can make the debugging process easier in the future, minimize the potential code expansion and maximize the speed improvement. On average, a program often spends 80% of its execution time on 20% of its code. As a software developer, the goal is to find out the 20% of the code and try to slim them down with inline (except loop recursion).

5.6 Clause 31: minimize the compilation dependency between files

C + + doesn't do a good job of "separating the interface from the implementation", as follows

class Person {
public:
    Person(const std::string& name, const Date& birthday, const Address& addr);
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
private:
    std::string theName;
    Date theBirthDate;
    Address theAddress;
};

The above class Person cannot be compiled because the compiler does not get the definitions of class string,Date and Address used in the implementation code. These definitions are often provided by #include indicators, as follows

#include <string>
#include "date.h"
#include "address.h"

However, there is a compilation dependency between the Person definition file and the included file. If the header file changes, each file containing the header file must be recompiled. This compilation dependency can cause unspeakable disaster. The solution is to divide the Person into two classes, one responsible for providing the interface and the other responsible for implementing the interface. Generally, only the inlcude interface is required when using class outside.

#include <string>
#include <memory>
class PersonImpl;    // Pre declaration of Person implementation class
class Date;    // The pre declaration of the class used by the Person interface
class Address;
class Person {
public:
    Person(const std::string& name, const Date& birthday, const Address& addr);
    std::string name() const;
    std::string birthDate() const;
    std::string address() const;
private:
    std::shared_ptr<PersonImpl> pImpl;    // Pointer to the implemented object
};

Here, the Person class only contains a pointer member to its implementation class. The pointer is used because C + + must know the size of Person, and the pointer size is determined, but the object size cannot be determined in this file. This design is called pimpl idiom(pointer to implementation). Under this design, the client of Person is completely separated from the implementation details of Date Address Person. Any modification of the Person implementation class does not require the recompilation of the Person client. The customer cannot see the specific implementation of Person, let alone write the code that depends on the specific implementation. The essence of the real "separation of interface and implementation" compilation dependency minimization: in reality, let the header file satisfy itself as much as possible. In case of failure, let it depend on the declaration (rather than definition) in other files. The policy requirements are as follows:

  1. If the task can be completed by using object reference or object pointer, do not use object. Reference and pointer pointing to the type can be defined only by the type declaration, but if you define an object, you need to define the declaration.
  2. If possible, try to replace class definitions with class declarations. When a class is used to declare a function, the definition of class is not required.
class Date;    // class declarative
Date today();    // Just declarative
void clearAppointments(Date d);    // Again, only declarative

There is no need to define Date for declaring today() function and clearAppointments() function, but once these functions are called, the Date definition needs to be exposed before calling. 1. Provide different header files for declarative and definable. Declarative header files and definable header files must be consistent. Customers only need #include one declarative file without declaring several functions in advance.

#include "datewfd.h" / / this header file declares but does not define class Date
Date today();
void clearAppointments(Date d);

In order to access variables or objects in other compilation units (such as another code file), for common types (including basic data classes, structures and classes), you can use the keyword extern to use these variables or objects; For template types, you must use the newly added keyword export (export / export / output) in standard C + + when defining these template class objects and template functions.

The class that uses pimpl idiom like Person is called Handle class. To use the Person class, you need to transfer all its function behavior to the implementation class PersonImpl. When implementing the Person class, you need to include the interface class and implement the interface class at the same time

#Include "person. H" / / include person class definition
#include "PersonImpl.h" / / implementation class
Person::Person(const std::string& name, const Date& birthday, const Address& addr)
    : pImpl(new PersonImpl(name, birthday, addr)) {}
std::string Person::name() const
{
    return pImpl->name();
}

The above Person constructor calls the new PersonImpl construct and actually calls PersonImpl::name through Person::name().

Another way to create Handle class is to make Person a special abstract base class, called interface class. This class is used to describe the interface of derived class, so it usually has no member variables and no constructor, only a virtual destructor and a set of pure virtual functions.

class Person {
public:
    virtual ~Person();
    virtual std::string name() const = 0;
    virtual std::string birthDate() const = 0;
    virtual std::string address() const = 0;
};

Obviously, the application must be written in Person's pointer or reference, because it is impossible to manifest an entity for the Person class of the pure virtual function. Except that the interface of the interface is modified, the customer does not need to recompile. interface class usually uses factory functions to return pointers to dynamically allocated objects. Such functions are often declared as static

// Implementation class
class RealPerson: public Person {
public:
    RealPerson(const std::string& name, const Date& birthday, const Address& addr)
        theName(name),theBirthDate(birthday), theAddress(addr) {}
    virtual ~Person() {};
    virtual std::string name() const;
    virtual std::string birthDate() const;
    virtual std::string address() const;
private:
    std::string theName;
    Date theBirthDate;
    Address theAddress;
};
// Factory class
class Person {
public:
    static std::shared_ptr<Person> create(const std::string& name,
        const Date& birthday, const Address& addr)
    {
        return std::shared_ptr<Person>(new RealPerson(name, birthday, addr));
    }
};

Above, the coupling relationship between interface and implementation is contacted through Handle class and interface class. The Handle class uses the interface class to contain a pointer to the implementation class to contact the coupling between the interface and the implementation. Interface class uses pure virtual function interface, and the implementation class only needs to be interface class. The program header file should exist in the form of "full and explicit only".

6, Other

Right value reference is one of the main new features added in C++11. It's not easy to understand. Let's sort it out here.

6.1 left and right values

Wikipedia defines the "value categories" attribute of C + + expressions as left or right values. The left value is the value of the expression corresponding to the object with the determined storage address in memory, while the right value is the value of all expressions that are not left values. Therefore, the right value can be an expression such as a literal, a temporary object, etc.

Whether it can be assigned or not is not the basis for distinguishing the left value and right value of C + +. The const left value of C + + is not assignable; The right value as a temporary object may be allowed to be assigned. The fundamental difference between lvalue and rvalue is to obtain the corresponding memory address through the & operator.

C + + expressions are either lvalues or rvalues. Note that literals and variables are the simplest expressions in C + +, and the result is the value of literals and variables.

int i = 0, j = 0, k = 0;    //Initialization instead of assignment
const int ci = i;   //Initialization instead of assignment

1024 = k;   //Error: literal is a right-hand value
i + j = k;  //Error: arithmetic expression is an R-value
ci = k;     //Error: ci is a constant lvalue and cannot be modified

Generally speaking, an lvalue expression represents the identity of an object, and an lvalue expression represents the value of the object. Different operators have different requirements for operands. Some require lvalue operands, and some require rvalue operands; There are also differences in return values. Some get left value results and some get right value results. An lvalue cannot be bound to an expression that requires conversion, a literal constant, or an expression that returns an lvalue, but an lvalue reference can be bound to these expressions. An R-value reference cannot be bound directly to an l-value. Note that although the right value cannot obtain the address, the right value reference can obtain the address, which represents the storage location of the temporary object.

int i = 42; // Initialization, declaration + definition + assignment
int &r = i; // Reference, lvalue
int &&rr = i;   // Error, cannot bind an R-value reference to an l-value
int &r2 = i * 42;   // Error, i*42 is an R-value expression, and an l-value reference cannot bind an R-value
const int &r3 = i * 42; // Correct, you can bind a const reference to an rvalue
int &&rr2 = i * 42; // Correct, bind to the right value expression

The right value reference can obtain the address. Although the left value reference cannot bind the right value, it can bind the right value reference

int &&r = 10;  
int &l = r;
l = 11;
cout << r << endl;  // 11

Another way to understand lvalue and lvalue is through registers and memory, such as constant 5. When used, it will not allocate space in memory, but directly put it into registers, so it is an lvalue in C + +. Define a variable a, which will allocate space in memory, which is the lvalue in C + +. As for a+5, because the result of a+5 is stored in a register, it does not allocate new space in memory, so it is an R-value.

Because the right value reference can only be bound to temporary objects, not variables, you can know that the object referenced by * will be destroyed

Although you cannot bind an R-value reference to an l-value, you can explicitly convert an l-value to the corresponding R-value reference type.

int &&rr3 = std::move(rr1);

The move call tells the compiler that we have an lvalue, but we want to use it like an lvalue. This means a commitment not to use rr1 except to assign or destroy it.

6.2 mobile constructor

The move constructor moves elements from one object to another, rather than copying them.

StrVec::StrVec(StrVec &&s) noexcept
    : elements(s.elements), first_free(s.first_free)
{
    s.elements = s.first_free = nullptr;
}

Above, the move constructor does not allocate any new memory. Its function is actually to move the pointer, that is, it can transfer the pointer members in one object to another object. After the pointer member is transferred, the pointer member in the original object is generally set to NULL to prevent it from being used again.

The idea behind the mobile assignment operation is that "assignment" does not have to be done through "copy", but can also be realized by simply "swapping" the source object to the target object. For example, for the expression s1=s2, instead of copying word by word from s2, we can directly let s1 "occupy" the data storage in s2 and "delete" the original data storage in s1 in some way (or simply throw it to s2, because in most cases s2 will be destructed later). The move constructor passes in an R-value reference, which is also used to move data. Because the left value is only an alias, the right value is the real value.

6.3 std::move

std::move is a type converter that converts an lvalue to an lvalue.

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
    return static_case<typename remove_reference<T>::type&&>(t);
}

The input parameter type of move is called a generic reference type. The so-called general reference refers to the reference modified by auto or template. It can accept lvalue reference and lvalue reference. In fact, auto is the T in the template, and they are equivalent. The reason why l-value reference and R-value reference can be accepted is through template derivation.

template<typename T>
void f(T&& param){
    std::cout << "the value is "<< param << std::endl;
}

int main(int argc, char *argv[]){

    int a = 123;
    auto && b = 5;   //General reference, can receive right value

    int && c = a;    //Error, right value reference, cannot receive left value

    auto && d = a;   //General reference, which can receive lvalues

    const auto && e = a; //Error, adding const is no longer a general reference

    func(a);         //General reference, which can receive lvalues
    func(10);        //General reference, can receive right value
}

6.4 template derivation and implementation of move

Template type derivation is divided into two categories: templates that are not reference or pointer types are one category; Reference and pointer templates are another type.

For the first type, the derivation is based on the principle that the value passed by the function parameter does not affect the original value, so no matter whether the parameter you actually pass in is an ordinary variable, constant or reference, it will eventually degenerate into the original type without any modification. For example, after const int & type is passed in, it degenerates into int type.

The second type is template, and the type is reference (including lvalue reference and rvalue reference) or pointer template. The principle of this type derivation is to remove the equivalent number of reference symbols, and other keywords are the same. For example, the type of X in func(x) is int &, and it can be known that t is int together with T &. Another example is function(x), where x is int & it can be seen that t is int & together with T &. Note that at this time, int & & is folded into int & & by reference, so t is int&

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

template <typename T>
void func(T& param);

template <typename T>
void function(T&& param);

6.5 general references

template<typename T>
void f(T&& param){
    std::cout << "the value is "<< param << std::endl;
}

The incoming type T & & is a general reference because the template can deduce types. Therefore, it can receive both left and right values correctly. When the left value and the general reference are placed together, the derived t is still the left value, while the right value and the general reference are placed together, the derived t is still the right value. Reference collapse int & & collapse as int & int & & & collapse as int & int & & collapse as int & int & & & collapse as int&&

In C++11, a member called type member is added. Like static members, type members belong to a class rather than an object. When accessing it, they are also accessed with:: as static members. For example:

typename remove_reference<T>::type&&
typedef std::string::size_type type_s;

template <typename T>
typename T::value_type top(const T &c){}    // Returns a T::value_type type

remove_ The implementation of the reference class extracts the type, so that the right value reference will be obtained no matter whether the left value reference, right value reference or value is passed in. remove_reference uses the automatic derivation of the template to obtain the type after the argument is referenced.

template <typename T>
struct remove_reference{
    typedef T type;  //Define the type alias of T as type
};

template <typename T>
struct remove_reference<T&> //lvalue reference 
{
    typedef T type;
}

template <typename T>
struct remove_reference<T&&> //rvalue reference 
{
   typedef T type;
}

6.6 perfect forwarding

Through the move function, we can unconditionally convert a general reference into an R-value reference, so as to facilitate moving semantics, prevent copying and improve efficiency. Some functions need to forward their parameters and the same type to other functions instead of unconditionally converting them to r-values. Perfect forwarding is used at this time.

Although the general reference can ensure that the incoming left value deduces the left value and the incoming right value deduces the right value, the possible temporary variables may make it ineffective.

template<typename T>
void func(T& param) {
    cout << "An lvalue was passed in" << endl;
}
template<typename T>
void func(T&& param) {
    cout << "The right value is passed in" << endl;
}
template<typename T>
void warp(T&& param) {
    func(param);
}
int main() {
    int num = 2019;
    warp(num);
    warp(2019);
    return 0;
}

// output
 An lvalue was passed in
 An lvalue was passed in

The formal parameter of the warp () function itself is a general reference, which can accept both left and right values; The first warp () function call argument is the left value, so the parameter introduced in func () in the warp () function should also be the left value. The argument of the second warp() function call is an R-value. According to the reference folding rule, the parameter type received by the warp() function is an R-value reference. Why did you call the l-value version of func()? This is because inside the warp() function, the lvalue reference type changes to an lvalue, and the parameter has an lvalue name. In fact, the variable address is obtained through the variable name, and the lvalue reference is passed in.
Keep the variable type unchanged during function call, which is the so-called "perfect forwarding" technology. In C++11, the function forwards the argument type unchanged through the std::forward() function.

template<typename T>
void warp(T&& param) {
    func(std::forward<T>(param));
}

Topics: architecture