Clauses 23 and 24

Posted by lettie_dude on Tue, 04 Jan 2022 20:21:38 +0100

Clause 23: replace member function with non member and non friend
Why replace member functions with non member functions and non friend functions? In fact, this is to ensure the encapsulation of data. How does the encapsulation of data reflect? A rough measurement. We think that the more functions can access it, the lower the encapsulation of data. If the data changes, because its change involves too many changes, its encapsulation is poor.
Compared with member functions, non member functions and non friend functions cannot access the data in private, so the encapsulation of data is naturally better. In other words, the more functions can be accessed, the lower the encapsulation of data.

It should be noted that if a function "becomes a non member of a class" just because it cares about encapsulation, it does not mean that it "cannot be a member of another class". This function can become a static member function of another class as long as it does not affect the encapsulation of private members in the original class.

Take a look at the examples in the book:

class WebBrowser {
public:
	void clearCache();
	void clearHistory();
	void removeCookies();
}

Now there is a class that provides three methods to clear some caches of web browsers. If we want to clear Cache, History and Cookies together, we can put these three functions in a member function, so that we only need to call one function at a time.

class WebBrowser{
public:
	void clearEverything(); //Call clearCache(),clearHistory(),removeCookies()
}

But clearEverything() is a member function. Doesn't the clause say that it's better to replace member with non member and non friend? Therefore, this can be done:

void clearBrowser(WebBrowser &wb) {
	wb.clearCache();
	wb.clearHistory();
	wb.removeCookies();
}

Make it a non member function.
In c + +, we can usually put the class and non member functions related to the class in the same namespace, like this:

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

At the same time, there may be multiple convenience functions related to the WebBrowser class, as well as multiple types, such as those related to cookies and bookmarks. In order to reduce unnecessary dependencies (for example, I only want to use functions related to cookies, so it is not necessary to introduce functions related to bookmarks), we can declare them in different header files, but they all belong to the namespace of WebBrowserStuff, because they are related to WebBrowser.

//The header file "webbrowser.h" is aimed at the class WebBrowser itself and the core functions of the WebBrowser.
namespace WebBrowserStuff {
	class WebBrowser {...};
	... 	//Core functions, such as the non member function required by almost all customers
}

//Header file "webbrowserbookmarks.h"
namespace WebBrowserStuff {
	...		//Convenience functions related to bookmarks
}

//Header file "webbrowsercookies.h"
namespace WebBrowserStuff {
	...		//cookie related convenience functions
}

Putting convenience functions in multiple header files but belonging to the same namespace means that we can easily add other convenience functions on this basis. This is also the better point of namespace than class. Class is a whole and cannot be extended for users, but the name space can.

Clause 24: if all parameters need type conversion, please use the non member function for this purpose
Now we define a class of rational numbers:

class Rational {
public:
	Rational(int numerator = 0, int denominator = 1);
	int numerator() const;
	int denominator() const;
private:
	...
}

It seems normal to multiply rational numbers. Therefore, overload the multiplication operation in the class:

class Rational {
public:
	...
	const Rational operator* (const Rational &rhs) const;
}

This supports the multiplication of two rational numbers:

Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf * oneEighth;
result = result * oneEighth;

It seems normal to multiply rational numbers and integers when we execute:

result = oneHalf * 2;//correct
result = 2 * oneHalf;//error

It can be found that one of them reports an error because oneHalf is an object containing the class of the operator * function, so the compiler calls the function. The integer 2 has no corresponding class, so there is no operator * member function. The compiler tries the non member function operator * (that is, in the namespace or in the global scope), but there is no relevant definition elsewhere, so an error is reported. Let's look at result = oneHalf * 2. Why is this correct? In fact, an implicit type conversion is involved here, that is, 2 is converted to a Rational type through the constructor, and the two Rational types are multiplied. Naturally, there is no error.
Only when the parameter is listed in the parameter column, this parameter is a qualified participant in implicit type conversion, just as result = oneHalf * 2. In case of error, we can overload the multiplication of a non member function:

class Rational {
	...
};
const Rational operator*(const Rational& lhs, const Rational& rhs) {
	return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}

In fact, the above two overload methods are called in class overload and out of class overload. When the two overload methods exist at the same time, the compiler will give priority to the use of in class overload, because the in class overload is first visible to the class user, so its priority is higher (for the class user, the priority must be the intended one).

Topics: C++