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
|
|
A fourth one needs attention.
|
|
This call is a call to the copy constructor.
Try to replace #define with const, enum, inline
For example, the following define is defined.
|
|
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.
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.
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.
|
|
In the above case, the Directory references the tfs object, but it may not be initialized because it is compiled in a different unit.
|
|
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.
The copy assignment operator is not generated if a class contains const members or reference members.
|
|
Exceptions are thrown during compilation.
|
|
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.
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.
Then use polymorphism to return a base class pointer object.
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.
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.
Then there is BuyTransaction which inherits from Transaction.
Then the BuyTransaction constructor is executed.
|
|
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.
|
|
Placing objects into smart pointers with separate statements
For example for a method like this.
|
|
If it is written like this it will pass compilation.
|
|
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.
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.
|
|
If a function is set at this time that requires pass-by-value.
|
|
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.
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.
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
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.
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
|
|
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.
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.
If at this point there is a D code that inherits B and implements the mf method inside D as well.
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.
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.
The above code SquareBase::invert
is public, so only one copy of the code SquareBase<double>::invert
will appear in the above example.