Recently, I was reading the book “Effective C++” and found that many of the concepts are likely to be forgotten if I just read it once, so I simply organized the excerpts. This article is mainly an excerpt of some of the more simple and easy to operate regulations.

Assignment and construction

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Weight {
public :
    Weight();                               // defualt constructor
    Weight(const Weight& rhs);              // copy constructor
    Weight& operator=(const Weight& rhs);   // copy assignment operator
    ...
}

Weight w1;                                  // Calling the defualt constructor
Weight w2(w1);                              // Calling the copy constructor
w1 = w2;                                    // Calling the copy assignment operator
Weight w3 = w2;                             // Calling the copy constructor

A fourth one needs attention.

1
Weight w3 = w2;

This call is a call to the copy constructor.

Try to replace #define with const, enum, inline

For example, the following define is defined.

1
#define ASPECT_RATIO 1.653

This notation is replaced by the preprocessor before the compiler starts processing the source code, so when this constant is applied and you get a compile error message, it may mention 1.653 instead of ASPECT_RATIO, so you may waste unnecessary time tracking it down.

And if ASPECT_RATIO is used in more than one place then it will be replaced by the preprocessor with multiple copies, which does not happen with constants.

On the other hand, #define does not provide encapsulation, there is no such thing as private#define, but const can be encapsulated.

Then again, macros that look like functions should ideally be replaced with inline functions, because if such macros are used there are too many problems when using them, which may lead to unintended results.

Use const whenever possible

const can add a mandatory constraint, and this constraint can reduce the cost of understanding the code. For example, const acts on member functions to know which function can change the content of the object and which function cannot.

The const syntax, despite its many variations, basically obeys the following rules.

  • If the keyword const appears to the left of an asterisk, it means that the referee is a constant.
  • If it appears to the right of an asterisk, it means that the pointer itself is a constant.
  • If it appears on either side of the asterisk, it means that both the object being referred to and the pointer are constants.

const can also be associated with the return value of a function, each parameter, and the function itself. For example, making a function return a constant value can reduce the number of exceptions caused by client errors without giving up safety and efficiency.

1
2
3
Rational a,b,c;
...
if (a * b = c) // I meant to type == but it became = . const to avoid this error

Make sure that the object is initialized before it is used

Because c++ does not guarantee that an object is necessarily initialized when it is used, for example.

1
2
3
4
5
6
7
int x;

class Point{
    int x, y;
}
...
Point p;

The above code will be initialized in some contexts, sometimes not. c++ does not guarantee initialization, so it is best to initialize it manually before using it.

When initializing, it is better to use the initialization list rather than the assignment initialization. The initialization operation will precede the assignment operation, so the initialization is more efficient. The assignment-based operation will first call the default constructor to set the initial values for theName and theAddress, and then immediately assign new values to them.

Members are initialized in the same order as they appear in the class definition: the first member is initialized first, then the second, and so on. So it is best to make the order of initializing constructors consistent with the order of member declarations. And if possible, try to avoid using some members to initialize other members.

For the initialization of non-local static, c++ also does not specify the initialization order of non-local static objects in different compilation units, so a non-local static object may be uninitialized if it uses a non-local static object in another compilation unit. So we can use local static instead of non-local static object.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class FileSystem {
public:
    ...
    std::size_t numDisks() const;
}
extern FileSystem tfs; // Objects prepared for use in other units

------------------------------------- 
//Another compilation unit
class Directory{
public 
    ...
    Directory(params);
    ...
}
Direcotry::Directory(params){
    ...
    std::size_t disk=tfs.numDisks(); //Use tfs objects that may not be initialized
}

In the above case, the Directory references the tfs object, but it may not be initialized because it is compiled in a different unit.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class FileSystem {
public:
    ...
    std::size_t numDisks() const;
}
FileSystem& tfs()
{
    static FileSystem fs; //Initialize and return a local static object
    return fs;
}

------------------------------------- 
//Another compilation unit
class Directory{
public 
    ...
    Directory(params);
    ...
}
Direcotry::Directory(params){
    ...
    std::size_t disk=tfs().numDisks(); //Use tfs objects that may not be initialized
}

The above changes the non-local static object to a local static object to avoid the initialization problem.

C++ default functions

For an empty class, C++ generates a copy constructor, a default constructor, a copy assignment operator, and a destructor by default.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// If writing down
class Empty {}

// The following functions are taken on by default
class Empty {
public:
    Empty() { ... }
    Empty(const Empty& rhs) { ... }
    ~Empty() { ... }
    Empty& operator=(const Empty& rhs) { ... }
};

The copy assignment operator is not generated if a class contains const members or reference members.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
template<class T>
class NamedObject {
public:
    NamedObject(std::string & name, const T& value);
private:
    std::string & nameValue;
    const T objectValue;
};

int main() 
{ 
    std::string newDog("perse");
    std::string oldDog("satch");
    NamedObject<int> p(newDog ,2);
    NamedObject<int> s(oldDog ,36);
    p = s;
} 

Exceptions are thrown during compilation.

1
2
/home/luozhiyun/data/cpptest/main.cpp:6:7: error: non-static reference member 'std::string& NamedObject<int>::nameValue', can't use default assignment operator
/home/luozhiyun/data/cpptest/main.cpp:6:7: error: non-static const member 'const int NamedObject<int>::objectValue', can't use default assignment operator

Since some functions are generated by default, you should explicitly reject them when you don’t want to use them. For example, declare a private copy constructor function or prevent copy in the parent class, as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Uncopyable {
protected:
    Uncopyable() {}             //Allow construction and destructuring
    ~Uncopyable() {}
private:
    Uncopyable(const Uncopyable&); //Blocking Copying
    Uncopyable& operator=(const Uncopyable&);
}

class HomeForSale:private Uncopyable{ 
 ...
}

Declaring virtual destructors for polymorphic base classes

If you design a base class destructor that is not virtual, and then have many subclasses inherit this base class, and then use the polymorphic feature to dynamically allocate a pointer to the base class to point to the subclass, and then release this pointer, the subclass object will probably not be destroyed.

1
2
3
4
5
6
7
8
class TimeKeeper {
public:
    TimeKeeper();
    ~TimeKeeper();
    ...
};

class AtomicClock : public TimeKeeper{...} // Atomic Clock Inheritance TimeKeeper

Then use polymorphism to return a base class pointer object.

1
2
3
TimeKeeper* ptk = getTimeKeeper(); // Get a dynamically allocated object
...                                // Run
delete ptk;                        // release, which may result in subclass objects that are not destroyed

The ptk pointer above actually points to an AtomicClock object, the base class has a non-virtual destructor, and the destruction performed by the pointer of the base class will cause the subclass object pointed to by ptk not to be destroyed, thus forming a resource leak.

So when a base class is intended for polymorphic use it should have a virtual destructor. If the base class is not used as polymorphic, like the Uncopyable class above, there is no need to declare a virtual destructor.

Destructors should not throw exceptions

If a function called by a destructor may throw an exception, the destructor should catch any exceptions and then swallow them or end the program.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class DBConn {
public:
 ...
 void close()
 {
    db.close();
    closed = true;
 }
 ~DBConn()
 {
    if(!closed) {
        try {
            db.close();
        }catch(...) {
            ...
        }
    }
 }
private:
    DBConnection db;
    bool closed;
}

Do not call virtual functions in constructors and destructors

If there is a base class Transaction, the virtual function is called in the constructor, as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Transaction {
public:
    Transaction();
    virtual void logTransaction() const ; 
};

void Transaction::logTransaction() const{
    cout<< "Transaction" << endl;
}

Transaction::Transaction()
{ 
    logTransaction();
}

Then there is BuyTransaction which inherits from Transaction.

1
2
3
4
5
6
7
8
class BuyTransaction: public Transaction {
public:
    virtual void logTransaction() const; 
};

void BuyTransaction::logTransaction() const{
    cout<< "BuyTransaction" << endl;
}

Then the BuyTransaction constructor is executed.

1
BuyTransaction b;

At this point, since the Transaction constructor is executed before the BuyTransaction constructor, logTransaction is actually called on the Transaction version, not the BuyTransaction version.

Remember to copy the base class when copying the object

For example, if you have a PriorityCustomer object that inherits a Customer object as a base class with copy constructor and copy assignment operator, remember to copy the base class object when implementing these two functions, otherwise the default initialization action will be performed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class PriorityCustomer: public Customer {
public:
    ...
    PriorityCustomer(const PriorityCustomer& rhs);
    PriorityCustomer& operator=(const PriorityCustomer& rhs);
    ...
private:
    int priority;
};

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
    : Customer(rhs),  //Call the base class copy constructor
    priority(rhs.priority)
{
    logcall("PriorityCustomer copy constructor");
}

PriorityCustomer & PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
    logcall("PriorityCustomer copy assignment operator");
    Customer::operator=(rhs); //Copying of base class member variables
    priority = rhs.priority;
    return *this;
}

Placing objects into smart pointers with separate statements

For example for a method like this.

1
void processWidget(std::shared_ptr<Widget> pw, int priority);

If it is written like this it will pass compilation.

1
processWidget(std::shared_prt<Widget>(new Widget), priority());

This code executes the new widget, calls the priority function, and calls the shared_prt construct. The order in which they are executed is actually indeterminate.

If the new widget is executed first and then an exception is thrown when the priority call is executed, then the created object is likely to leak. The best practice should be as follows.

1
2
std::shared_prt<Widget> pw(new Widget)
processWidget(pw, priority());

Replace pass-by-value with pass-by-reference-to-const whenever possible

pass-by-value is an extremely expensive operation that by default calls the copy constructor of the object as well as the constructor of the parent class.

As follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
public: 
    Person();
    virtual ~Person();
    virtual std::string say() const;
    ...
private:
    std::string name;
    std::string address;
};

class Student: public Person {
public:
    Student();
    ~Student();
    virtual std::string say() const;
    ...
private:
    std::string schoolName;
    std::string schoolAddress;
}

If a function is set at this time that requires pass-by-value.

1
bool validateStudent(Student s);

Then when this function is called, it calls the copy constructor of Student, the constructors of the two member variables within Student, and the constructor of the parent class Person and its member variable constructs. So a total of 6 copy constructors will be called.

And pass-by-value also has the problem of object cutting, for example, if I write another function.

1
2
3
4
5
6
void saySomething(Person p){
    p.say();
}

Student s;
saySomething(s);

The above declares a function saySomething, and then creates the object Student, but in fact, because it is pass-by-value, the specialization information of Student will be excised, and the saySomething function will end up calling the say method of Person.

All the above solutions can be solved by pass-by-reference-to-const.

1
2
3
void saySomething(const Person& p){ // nice ,Parameters will not be cut
    p.say();
}

Since pass-by-reference actually passes a pointer, it has the advantage of being efficient and unaffected by the cut problem.

Do not return a reference in these cases

  • Do not return a pointer or reference to a local stack object, which is well understood, as the local stack object will be destroyed after the function returns.

  • Do not return a reference to a heap-allocated object, which may cause a memory leak

    1
    2
    3
    4
    5
    6
    7
    8
    
    const Rational& operator* (const Rational& lhs, const Rational& rhs)
    {
    Rational* result = new Rational(lhs.n * rhs.n,lhs.d * rhs.d);
    return *result;
    }
    
    Rational w,x,y,z;
    w = x * y * z;
    

    the above example calls operator* twice, so it calls new twice but returns only one object, then the other object cannot be deleted, and then it leaks.

  • Do not return a pointer or reference pointing to a local static object when multiple such objects are needed, local static is unique

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    const Rational& operator* (const Rational& lhs, const Rational& rhs)
    {
    static Rational  result;
    result = ...;
    return result;
    }
    
    bool operator==(const Rational& lhs, const Rational& rhs);
    
    Rational a,b,c,d;
    if ((a * b) == (c *d)) {// will always be true here, and the static object returned by operator* is unique
    
    }
    

Try to avoid transformations

  • Avoid transformations if possible, especially for performance requirements, and avoid using dynamic_casts.
  • Do not use old-style transformations, and try to use C++-style transformations.

Do not return a handle to an object’s internal

Do not return handles to private members of an object. Handles here include references, pointers and iterators. This adds encapsulation to the class, makes const functions more const, and avoids the creation of empty references (dangling handles).

For example.

1
2
3
4
5
6
7
8
class Rectangle {
private:
  int _left, _top;
public:
  int& left() { return _left; }
};
Rectangle rec;
rec.left() = 3; //Causes internal objects to be tampered with, breaking encapsulation

Another example is the problem of null references caused by a function that returns a temporary object but still holds the temporary object after it has been destroyed.

Exception-safe functions

Exception-safe functions do not leak any resources and do not allow data to be corrupted when an exception is thrown. Exception-safe functions provide one of the following three guarantees.

  • Basic promise: If an exception is thrown, anything within the program remains in a valid state. No objects or data structures will be corrupted as a result.
  • Strong promise: If an exception is thrown, the state of the program does not change. That is, atomicity is maintained and either the function succeeds or it fails.
  • No exceptions thrown guarantee: promises to never throw exceptions because they always do what they originally promised to do.

inline

Inlining generally makes programs faster, but it is not absolute. It can increase the size of a program, and if inlining is overzealous enough to make a program too large, it can lead to swap page behavior, reducing the cache hit rate, which in turn can reduce runtime efficiency.

inline is only a deep love for the compiler, not a mandatory command. It is generally defined within the header file, and during compilation a function call is awarded to replace the body of the function being called.

So the compiler must know what the function looks like, which is not valid for virtual functions because it waits until runtime to determine which function to call.

Since even an empty constructor initializes all members by default and calls the parent class constructor, it looks like a small amount of code, but the amount of code that actually compiles depends on the class member variables and whether there is inheritance. So the constructor being inline may cause a lot of code bloat, and it is usually not a good practice to inline constructors.

Also, don’t declare function templates as inline just because they appear in the header file; instead, declare them as inline only if you think that all the functions that appear according to this template should be inlined.

Note the inheritance masking problem

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Base {
public:
    virtual void mf1() = 0;
    virtual void mf1(int);
    virtual void mf2();
    void mf3();
    void mf3(double);
};

class Test : public Base{
public:
    virtual void mf1();
    void mf3();
    void mf4();
};

int main()
{
  Test t;
  int x;

  t.mf1();   //yes
  t.mf1(x);  //no, Test::mf1 Masked Base::mf1
  t.mf2();   //yes
  t.mf3();   //yes
  t.mf3(x);  //no,Test::mf3 Masked Base::mf3
}

The compiler will first look for local by name, then for the parent class Base, then for the namespace scope containing Base if it doesn’t have it, and finally for the global scope.

In addition to this, there is a name masking rule, where functions contained in a subclass will mask functions with the same name in the parent class, so Base::mf1 and Base::mf3 are no longer inherited.

If you want to continue to inherit, you can use using.

1
2
3
4
5
6
7
8
class Test : public Base{
public:
    using Base::mf1;
    using Base::mf3;
    virtual void mf1();
    void mf3();
    void mf4();
};

This allows the Base::mf1 and Base::mf3 inheritance to continue to be used.

Distinguish between interface inheritance and implementation inheritance

  • The purpose of declaring a pure virtual function is to allow a derived class to inherit only the interface of the function. This can be done to force the derived class to implement the function.
  • The purpose of declaring an impure virtual function is to have the derived class inherit the interface and default implementation of the function. If the derived class does not implement the function, the parent class’s implementation of the function is used by default.
  • The purpose of declaring a non-virtual function is to make the derived class inherit the interface and a mandatory implementation of the function. Since the non-virtual function represents invariant over specialization, it should never be redefined in a derived class.

Never redefine non-virtual functions that are inherited

as follows.

1
2
3
4
5
class B {
public:
    void mf();
    ...
}; 

If at this point there is a D code that inherits B and implements the mf method inside D as well.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class D: public B {
public:
    void mf();
    ...
};

D x;
B* pb = &x;
D* pd = &x;

pb->mf(); //Calling B::mf
pd->mf(); //Calling D::mf

Then since mf is no n-virtual, they are all statically bound and will actually be surprised to be called in their own methods. So don’t define new non-virtual functions that are inherited.

Note the code bloat that comes with templates

For example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
template<typename T, int n>
class Square{
public:
    void invert();
};

Square<double, 5> s1;
Square<double, 10> s2;
s1.invert();    
s2.invert();

The Square template will instantiate two classes: Square<double, 5> and Square<double, 10>, which will each have the same invert method, which leads to code bloat.

The solution can be to abstract the bloated code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
template<typename T>
class SquareBase{
protected:
    void invert(int size);
};

template<typename T, int n>
class Square:private SquareBase<T>{
private:
    using SquareBase<T>::invert;
public:
    void invert(){ this->invert(n); }

The above code SquareBase::invert is public, so only one copy of the code SquareBase<double>::invert will appear in the above example.