Effective C++ Notes for Beginners

Original link: https://www.luozhiyun.com/archives/715

Please state the source for reprinting~, this article was published on luozhiyun’s blog: https://www.luozhiyun.com/archives/715

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

assignment and construction

 class Weight { public : Weight(); // defualt构造函数Weight(const Weight& rhs); // copy构造函数Weight& operator=(const Weight& rhs); // copy assignment 操作符... } Weight w1; // 调用defualt构造函数Weight w2(w1); // 调用copy构造函数w1 = w2; // 调用copy assignment 操作符Weight w3 = w2; // 调用copy构造函数

The fourth one needs attention:

 Weight w3 = w2;

This call is to call the copy constructor.

Try to replace #define with const, enum, inline

For example, the following define is defined:

 #define ASPECT_RATIO 1.653

This token will be replaced by the preprocessor before the compiler starts processing the source, so when using this constant and getting a compile error message, it might mention 1.653 instead of ASPECT_RATIO, so it’s possible to track it down and waste unnecessary time.

And if ASPECT_RATIO is used in multiple places, it will be replaced by multiple copies by the preprocessor, which is not the case with constants.

On the other hand, #define cannot provide encapsulation, there is no such thing as so-called private #define, but const can be encapsulated.

The next thing is that function-like macros should be replaced with inline functions, because if there are too many problems when using such macros, it may lead to unexpected results.

try to use const

const can add a mandatory constraint that can reduce the cost of code comprehension. For example, when const acts on member functions, you can know which function can change the content of the object and which function cannot.

Although the const syntax has changed a lot, it basically obeys the following rules:

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

Const can also be associated with function return values, parameters, and the function itself. For example, making the function return a constant value can reduce the impact caused by customer errors without sacrificing safety and efficiency.

 Rational a,b,c; ... if (a * b = c) // 本来想键入== 却变成= ,const可以避免这样的错误

Make sure the object has been initialized before being used

Because C++ does not guarantee that the object must be initialized when it is used, for example:

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

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

When initializing, it is better to use the initialization list for initialization, rather than assignment initialization. The initialization operation will be earlier than the assignment operation, so the initialization efficiency is higher. 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’s best to keep the order of constructor initializers 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++ does not specify the initialization order of non-local static objects in different compilation units, so a non-local static object uses a non-local static object in another compilation unit object, the object may not be initialized. So we can replace non-local static objects with local static.

 class FileSystem { public: ... std::size_t numDisks() const; } extern FileSystem tfs; //预备给其他单元使用的对象------------------------------------- //另一个编译单元class Directory{ public ... Directory(params); ... } Direcotry::Directory(params){ ... std::size_t disk=tfs.numDisks(); //使用tfs 对象,可能未被初始化}

The above situation is that the Directory refers to the tfs object, but it may not be initialized due to different compilation units.

 class FileSystem { public: ... std::size_t numDisks() const; } FileSystem& tfs() { static FileSystem fs; //初始化并返回一个local static 对象return fs; } ------------------------------------- //另一个编译单元class Directory{ public ... Directory(params); ... } Direcotry::Directory(params){ ... std::size_t disk=tfs().numDisks(); //使用tfs 对象,可能未被初始化}

Here, the non-local static object is changed to a local static object to avoid initialization problems.

C++ default function

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

 // 如果写下class Empty {}; // 默认会带上下面这些函数class Empty { public: Empty() { ... } Empty(const Empty& rhs) { ... } ~Empty() { ... } Empty& operator=(const Empty& rhs) { ... } };

If a class has const members or reference members no copy assignment operator is generated:

 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; }

When compiling, it will report an error:

 /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

Because some functions are generated by default, you should explicitly reject them when you do not want to use the default functions. For example, declaring a private copy constructor function or preventing copy in the parent class, as follows:

 class Uncopyable { protected: Uncopyable() {} //允许构造和析构~Uncopyable() {} private: Uncopyable(const Uncopyable&); //阻止copy Uncopyable& operator=(const Uncopyable&); } class HomeForSale:private Uncopyable{ ... }

Declare virtual destructor for polymorphic base class

If you design a base class destructor that is not virtual, then there are many subclasses that inherit the base class, and then use the polymorphism feature to dynamically allocate a base class pointer to the subclass, and then release the pointer, the subclass object Probably not destroyed.

 class TimeKeeper { public: TimeKeeper(); ~TimeKeeper(); ... }; class AtomicClock : public TimeKeeper{...} // 原子钟继承TimeKeeper

Then use polymorphism to return a base class pointer object:

 TimeKeeper* ptk = getTimeKeeper(); //获得一个动态分配对象... // 运行delete ptk; // 释放,可能会导致子类对象并没有销毁

The ptk pointer above actually points to the AtomicClock object, and the base class has a non-virtual destructor. At this time, the destruction by the base class pointer will cause the subclass object pointed to by ptk to not be destroyed, resulting in a resource leak.

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

Destructors don’t spit out exceptions

If a function called by a destructor might throw exceptions, the destructor should catch any exceptions and either swallow them or end the program.

 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:

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

Then there is BuyTransaction which inherits Transaction:

 class BuyTransaction: public Transaction { public: virtual void logTransaction() const; }; void BuyTransaction::logTransaction() const{ cout<< "BuyTransaction" << endl; }

Then execute the BuyTransaction constructor:

 BuyTransaction b;

At this time, since the constructor of Transaction will be executed before the constructor of BuyTransaction, logTransaction is actually called the version of Transaction, not the version of BuyTransaction.

Remember to copy the base class when copying an object

For example, there is a PriorityCustomer object that inherits the Customer object as the base class, which contains the copy constructor and the copy assignment operator, then remember to copy the base class object when these two functions are implemented, otherwise the default initialization will be performed.

 class PriorityCustomer: public Customer { public: ... PriorityCustomer(const PriorityCustomer& rhs); PriorityCustomer& operator=(const PriorityCustomer& rhs); ... private: int priority; }; PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) : Customer(rhs), //调用基类copy构造函数priority(rhs.priority) { logcall("PriorityCustomer copy constructor"); } PriorityCustomer & PriorityCustomer::operator=(const PriorityCustomer& rhs) { logcall("PriorityCustomer copy assignment operator"); Customer::operator=(rhs); //对基类成员变量进行复制priority = rhs.priority; return *this; }

Put objects into smart pointers in separate statements

For example for a method like this:

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

If it is written like this, it can be compiled by:

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

This code will execute the new Widget, call the priority function, and call the shared_prt constructor. The order of their execution is virtually indeterminate.

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

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

Try to replace pass-by-value with pass-by-reference-to-const

pass-by-value is an extremely expensive operation, and by default the copy constructor of the object and the constructor of the parent class are called.

as follows:

 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 to require pass-by-value:

 bool validateStudent(Student s);

Then when this function is called, the copy constructor of Student, the constructor of two member variables in Student, and the constructor of the parent class Person and its member variable construction will be called. So the copy constructor is called a total of 6 times.

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

 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 student’s specialization information will be cut off, and the saySomething function will eventually call the say method of Person.

The above solutions can all be solved by pass-by-reference-to-const:

 void saySomething(const Person& p){ // nice ,参数不会被切割p.say(); }

Because pass-by-reference actually passes a pointer, it has the advantage of being efficient and immune to slicing problems.

Do not return reference in these cases

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

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

     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;

    In the above example, operator* is called twice, so new is called twice but only one object is returned, then the other object cannot be deleted, then it will leak;

  • Do not return pointer or reference to a local static object and need multiple such objects at the same time, local static is unique

     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)) {// 这里会恒为true,operator*返回的static 对象唯一}

try to avoid transformation

  • If possible, try to avoid casting, especially for performance requirements, avoid using dynamic_casts;
  • Don’t use old-style casts, try to use C++ style casts;

Don’t return a handle inside an object

Do not return handles to object private members. The “handle” here includes references, pointers, and iterators. This increases class encapsulation, makes const functions more const , and avoids dangling handles.

for example:

 class Rectangle { private: int _left, _top; public: int& left() { return _left; } }; Rectangle rec; rec.left() = 3; //导致内部对象被篡改,破坏了封装性

For another example, a function returns a temporary object, but still holds the null reference caused by the temporary object after the temporary object is destroyed;

exception safe function

When exceptions are thrown, exception-safe functions do not leak any resources and do not allow data to be corrupted. Exception-safe functions provide one of the following three guarantees:

  • Basic promise: If an exception is thrown, everything within the program remains in a valid state. No object or data structure will be destroyed by this;
  • Strong guarantee: if an exception is thrown, the program state does not change. That is to maintain atomicity, either the function succeeds or the function fails;
  • No-Throw Guarantee: Promises never throw exceptions because they always do what they were originally promised.

inline

Inline can generally make the program run faster, but it is not absolute. It will increase the size of the program. If the program size is too large due to excessive enthusiasm for inlining, it will cause the program to appear swap page behavior and reduce the cache hit rate, which will reduce the operating efficiency.

inline is just an affection for the compiler, not a mandatory command. It is generally defined in the header file, and during the compilation process, a function call will be replaced with the body of the called function.

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

Since even an empty constructor will initialize all members by default and call the parent class constructor, it seems that the amount of code is small. In fact, the amount of compiled code depends on the member variables of the class and whether there is inheritance. So constructors being inlined may cause a lot of code bloat, and generally inlining constructors is not a good practice.

Also, don’t declare function templates inline just because they appear in a header file. Instead, you should think that all functions that are manifested according to this template should be inlined before they are declared inline.

Be aware of the masking problem of inheritance

 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 掩盖了Base::mf1 t.mf2(); //yes t.mf3(); //yes t.mf3(x); //no,Test::mf3 掩盖了Base::mf3 }

The compiler will first look for it locally according to the name, then to the parent class Base to find it, if not, it will go to the namespace scope containing Base to find it, and finally to the global scope to find it.

In addition, there is a name masking rule. Functions contained in subclasses will mask functions of the same name in parent classes, so Base::mf1 and Base::mf3 are no longer inherited.

If you want to continue inheritance, you can use using:

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

Thus Base::mf1 and Base::mf3 inheritance can continue to be used.

Distinguish between interface inheritance and implementation inheritance

  • The purpose of declaring a pure virtual function is to allow derived classes to inherit only the function’s interface. It is possible to force derived classes to implement this function;
  • The purpose of declaring an impure virtual function is for derived classes to inherit the function’s interface and default implementation. If the derived class does not implement this function, the function implementation of the parent class is used by default;
  • The purpose of declaring a non-virtual function is for derived classes to inherit the function’s interface and a mandatory implementation. Since a non-virtual function stands for invariant over specialization, it should never be redefined in a derived class.

Never redefine inherited non-virtual functions

as follows:

 class B { public: void mf(); ... };

If there is a D code that inherits B at this time, and also implements the mf method in D:

 class D: public B { public: void mf(); ... }; D x; B* pb = &x; D* pd = &x; pb->mf(); //调用B::mf pd->mf(); //调用D::mf

Then since mf is non-n-virtual, it is statically bound, and in fact, it will be very surprising to call each of them into their own methods. So do not newly define inherited non-virtual functions.

Beware of code bloat caused by templates

for example:

 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 instantiates two classes: Square<double, 5> and Square<double, 10> , and they each have the same invert method, which leads to code bloat.

The solution can be to extract the bloated code:

 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.

Scan code_Search joint communication style-white version 1

“Effective C++” notes for beginners first appeared on luozhiyun`s Blog .

This article is reproduced from: https://www.luozhiyun.com/archives/715
This site is for inclusion only, and the copyright belongs to the original author.