55 specific methods of Effective C + + to improve program and design

Posted by kaser on Mon, 28 Feb 2022 03:26:38 +0100

preface

Almost every class we write will have one or more constructors, a destructor and a copy assignment operator. It's hard to be particularly excited about these. After all, they are your basic tools for making a living, controlling basic operations, such as producing a new object and ensuring that it is initialized, getting rid of the old object and ensuring that it is properly cleaned up, and giving new values to the object. If these functions make mistakes, they can lead to far-reaching and unpleasant consequences throughout our entire class. So making sure they behave correctly is a matter of life and death. The guidance provided in this chapter allows us to put these functions together well to form the spine of classes.

Clause 05: understand which functions C + + silently writes and calls

class Empty {

};
Equivalent to the following classes:
class Empty {
public:
	Empty(){}                          //default constructor
	Empty(const Empty&rhs){}           //Default Copy Constructor 
	~Empty(){}                         //Default destructor
	                                   //virtual, see you later
	Empty& operator=(const Empty&rhs){}//copy assignment operator
};
class Empty {
public:
	Empty(){...}                          //default constructor
	Empty(const Empty&rhs){...}           //Default Copy Constructor 
	~Empty(){...}                         //Default destructor
	                                      //virtual, see you later
	Empty& operator=(const Empty&rhs){...}//copy assignment operator
};
Since a constructor is declared, the compiler no longer creates a default constructor for it. This is important
,It means that we design a class carefully class,Its constructor requires arguments, so we don't have to worry about compilation
 The constructor will not worry about adding a parameterless constructor to you and masking your version.
template<typename T>
class NameObject {
public:
	NamedObject(const char*name, const T&value);
	NamedObject(const std::string&name, const T&value);
private:
	std::string nameValue;
	T objectValue;
};
template<typename T>
class NamedObject {
public:
	//The following constructor no longer accepts a const name because of nameValue
	//Now it's a reference to non const string. The previous char * constructor
	//It's over because there must be a string to refer to.
	NamedObject(std::string& name, const T&value);
	//...         It is assumed that the operator is not declared=
private:
	std::string& nameValue;   //This is now a reference
	const T objectValue;      //This is a const now
};
Now consider what happens:
template<typename T>
class NamedObject {
public:
	//The following constructor no longer accepts a const name because of nameValue
	//Now it's a reference to non const string. The previous char * constructor
	//It's over because there must be a string to refer to.
	NamedObject(std::string& name, const T&value);
	//...         It is assumed that the operator is not declared=
private:
	std::string& nameValue;   //This is now a reference
	const T objectValue;      //This is a const now
};

std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject<int> p(newDog, 2);  

NamedObject<int> s(oldDog, 36);
p = s;

please remember

  • The compiler can secretly create default constructor, copy constructor, copy assignment operator and destructor for class.

Clause 06: if you don't want to use functions automatically generated by the compiler, you should explicitly refuse

All compiler generated functions are public. To prevent these functions from being created, we declare them ourselves
 They are, but there are no requirements here that require you to declare them as public. So you can
copy Constructor or copy assignment Operator declared as priate. 
class HomeForSale {
public:
...
private:
...
	HomeForSale(const HomeForSale&);          //Only declaration
	HomeForSale& operator=(const HomeForSale&);
};
If you accidentally member Function or friend Function, it's the connector's turn to complain.
class Uncopyable {
protected:
	Uncopyable(){}          //Allow derived objects to be constructed and destructed
	~Uncopyable(){}
private:
	Uncopyable(const Uncopyable&);     //But stop copying
	Uncopyable& operator=(const Uncopyable&);
};

To stop HomeForSale The object is copied, and the only thing we need to do is inherit Uncopyable:
class HomeForSale :private Uncopyable {
...
};

please remember

  • In order to reject the function automatically provided by the compiler, the member function of the response can be declared as private and not implemented. It is also a practice to use base class es like Uncopyable.

Clause 07: declare a virtual destructor for a polymorphic base class

class TimeKeeper {
public:
	TimeKeeper();
	~TimeKeeper();
	//...
};

class AtomicClock:public TimeKeeper{};   //atomic clock
class WaterClock :public TimeKeeper{};   //water clock
class WristWatch :public TimeKeeper{};   //Wristwatch
//Dynamic allocation object of TimeKeeper derived class
TimeKeeper* getTimeKeeper();    //Returns a pointer to a
For compliance factory The rules of the function are getTimeKeeper()The returned object must be in heap. 
Therefore, in order to avoid leaking memory and other resources, the factory Each object returned by the function is appropriate
 land delete It's important to lose.
TimeKeeper* ptk = getTimeKeeper();       //Inherit system from TimeKeeper
										 //Get a dynamic allocation object and use it
delete ptk;					 			 //Release it to avoid resource leakage
When polymorphism is used, if an attribute in a subclass is opened to the heap, the parent class pointer cannot be called when it is released
 The destructor code of the subclass.

The problem is that the pointer returned by getTimeKeeper points to a derived class object (such as AtomicClock), which is deleted through a base class pointer (such as a TimeKeeper * pointer), and the current base class (TimeKeeper) has a non virtual destructor.

Solution: change the destructor in the parent class to virtual destructor or pure virtual destructor.
class TimeKeeper {
public:
	TimeKeeper();
	virtual~TimeKeeper();
	//...
};

TimeKeeper* ptk = getTimeKeeper();
delete ptk;		    //Now, behave correctly
whatever class Just with virtual Functions are almost certain that there should also be one virtual Destructor
 If class Excluding virtual Function, usually indicating that it is not intended to be used base class. 
class Point {
public:
	Point(int xCoord, int yCoord);
	~Point();
private:
	int x, y;
};

To implement the virtual function, the object must carry some information, which is mainly used to determine which virtual function is called at run time. This information is usually indicated by a so-called vptr(virtual table pointer). vptr points to an array composed of function pointers, which is called vtbl(virtual table); Each class with a virtual function has a corresponding vtbl. When an object calls a virtual function, the function actually called depends on the vtbl referred to by the vptr of the object - the compiler looks for the appropriate function pointer.

The commonness between virtual destruct and pure virtual destruct:
You can solve the problem of releasing subclass objects from parent class pointers
 All need specific function implementation
 Difference between virtual destruct and pure virtual destruct:
If it is a pure virtual destructor, this class belongs to an abstract class and cannot instantiate an object
class AWOV {             
public:
	virtual ~AWOV() = 0;//Declare pure virtual destructor
};
//A definition must be provided for a pure virtual destructor:
AWOV::~AWOV(){}       //Definition of pure virtual destructor

please remember

  • Polymorphic (polymorphic) base classes should declare a virtual destructor. If class has any virtual functions, it should have a virtual destructor.
  • Classes are not designed to be used as base classes or to be polymorphic, so virtual destructors should not be declared.

Clause 08: don't let exceptions escape the destructor

C++Destructors are not prohibited from throwing exceptions, but they are discouraged.
class Widget {
public:
	~Widget(){}            //Suppose this might spit out an exception
};

void doSomething()
{
	std::vector<Widget> v; 
}						  //v is automatically destroyed here
When vector v If it is destroyed, it has the responsibility to destroy all its contents widgets. hypothesis v Including ten Widgets,
An exception is thrown during the destruct of the first element. The other nine Widgets Or should it be destroyed,
therefore v Their destructors should be called. But suppose that during which calls, the second Widget Deconstruction
 Throw an exception. Now there are two exceptions acting at the same time, which is right C++Too much for me.
class DBConnection {
public:
	static DBConnection create(); //This function returns the DBConnection object
	//In order to simplify the temporary parameters, an exception will be thrown if the online shutdown fails
	void close();
};
class DBConn {
public:
	~DBConn() {
		db.close();
	}
private:
	DBConnection db;
};
This allows the customer to write such code:
{
	...
	DBConn  dbc(DBConnection::create());
}
Just call close Success, everything is fine, but if the call causes an exception, DBConn The destructor will
 The destructor allows the exception to propagate, that is, it allows the destructor to leave. That causes problems because that's throwing
 A difficult problem.
--------------------------------------------------------------------
Two solutions can avoid this problem:
  • If close throws an exception, the program ends. This is usually done by calling abort:
DBConn::~DBConn() {
	try { db.close(); }
	catch (...) {
		//Make operation records and record the failure of the call to close
		std::abort();
	}
}
  • Swallow the exception caused by calling close:
DBConn::~DBConn() {
	try { db.close(); }
	catch () {
		//Make operation records and record the failure of the call to close
	}
}
Generally speaking, swallowing anomalies is a bad idea because it suppresses them"Some actions failed"Important information.
A better strategy is to redesign DBConn Interface, so that its customers have the opportunity to respond to possible problems.
class DBConn {
public:
	void close() {      //Provide new functions for users
		db.close();
		closed = true;
	}

	~DBConn() {
		if (!closed) {
			try {       //Close connection
				db.close();
			}
			catch (...) {
				//Make operation records and record the failure of the call to close
			}
		}
	}
private:
	DBConnection db;
	bool closed;
};

please remember

  • Destructors should never spit out exceptions. If a function called by the destructor may throw exceptions, the destructor should catch any exceptions and swallow them (without propagation) or end the program.
  • If a customer needs to respond to an exception thrown during the operation of an operation function, class should provide a normal (not in the destructor) to perform the operation.

Clause 09: never call the virtual function in the process of construction and destruct.

Should not be called during constructors and destructors virtual Function, because such a call will not bring the expected results.
class Transaction {
public:
	Transaction();
	virtual void logTransaction() const = 0;
};

Transaction::Transaction()   //Implementation of base class constructor
{
	logTransaction();
}

class BuyTransaction :public Transaction {
public:
	virtual void logTransaction()const;
};

class SellTransaction :public Transaction {
public:
	virtual void logTransaction() const;
};

please remember

  • Do not call virtual functions during construction and deconstruction, because such calls never descend to derived class

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

Available when a formal parameter has the same name as a member variable this Pointer discrimination
 Return the object itself in the non static member function of the class, but use return *this
class Widget {
public:
	Widget& operator=(const Widget&rhs) {    //The return type is a reference
											 //Point to current object
		return *this;						 //Return left object
	}
};

please remember

  • Make the assignment operator return a reference to *this.

Clause 11: Handling "self assignment" in operator =

class Base{};
class Derived:public Base{};
void doSomething(const Base& rb, Derived* pd);//rb and * pd may actually be the same object

please remember

  • Ensure that operator = behaves well when the object is self assigned. The technologies include comparing the addresses of "source object" and "target object", careful statement order, and copy and swap.
  • Determines whether any function behaves correctly if it operates on more than one object, and multiple objects are the same object.

Clause 12: don't forget every component when copying an object

class Base{};
class Derived:public Base{};
void doSomething(const Base& rb, Derived* pd);//rb and * pd may actually be the same object

void logCall(const std::string& funcName);
class Customer {
public:
	Customer(const Customer&rhs);
	Customer& operator=(const Customer&rhs);
private:
	std::string name;
};
If you are class To add a member variable, you must modify it at the same time coying Function. If you forget, the compiler is unlikely to remind you.
Once inheritance occurs, it may lead to the most hidden crisis of this theme:
class Date{};
class Customer {
public:
private:
	std::string name;
	Date lastTransaction;
};


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

PriorityCustomer::PriorityCustomer(const PriorityCustomer & rhs)
	:priority(rhs.priority)
{
	logCall("PriorityCustomer copy constructor");
}

PriorityCustomer & PriorityCustomer::operator=(const PriorityCustomer & rhs)
{
	logCall("PriorityCustomer copy assignment operator");
	priority = rhs.priority;
	return *this;
}

base class name and lastTransaction Not copied
At any time, as long as we take responsibility for it"derived class compose copying function"The heavy responsibility of,
It must also be copied with care base class Ingredients. Which ingredients are often private,therefore
 We can't access them directly. We should let them derived class of copying Function call corresponding
bass class Function:
PriorityCustomer::PriorityCustomer(const PriorityCustomer & rhs)
	:Customer(rhs),          //Call the copy constructor of base class
	priority(rhs.priority)
{
	logCall("PriorityCustomer copy constructor");
}

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

please remember

  • The Copying function should ensure that "all member variables in the object" and "all base class components" are copied.
  • Do not try to implement another copying function with one copying function. The common function should be put into the third function and called by two copying functions

Topics: C++ Back-end