<Item 22> Declare data members private
1、使数据成员private,保持了语法的一致性,client不会为访问一个数据成员是否需要使用括号进行函数调度,还是不使用括号直接访问成员而纠结。
2、使数据成员private,if you use functions to get or set its value, you can implement no access, read-only access, and read-write access. Heck, you can even implement write-only access if you want to:
class AccessLevels { public: ... int getReadOnly() const { return readOnly; } void setReadWrite(int value) { readWrite = value; } int getReadWrite() const { return readWrite; } void setWriteOnly(int value) { writeOnly = value; } private: int noAccess; // no access to this int int readOnly; // read-only access to this int int readWrite; // read-write access to this int int writeOnly; // write-only access to this int };
3、Still not convinced? Then it's time to bring out the big gun: encapsulation. If you implement access to a data member through a function, you can later replace the data member with a computation, and nobody using your class will be any the wiser.
4、The argument against protected data members is similar. In fact, it's identical, though it may not seem that way at first. The reasoning about syntactic consistency and fine-grained access control is clearly as applicable to protected data as to public, but what about encapsulation? Aren't protected data members more encapsulated than public ones? Practically speaking, the surprising answer is that they are not.Item 23 explains that something's encapsulation is inversely proportional to the amount of code that might be broken if that something changes. The encapsulatedness of a data member, then, is inversely proportional to the amount of code that might be broken if that data member changes。
5、Suppose we have a public data member, and we eliminate it. How much code might be broken? All the client code that uses it, which is generally an unknowably large amount. Public data members are thus completely unencapsulated. But suppose we have a protected data member, and we eliminate it. How much code might be broken now? All the derived classes that use it, which is, again, typically an unknowably large amount of code. Protected data members are thus as unencapsulated as public ones, because in both cases, if the data members are changed, an unknowably large amount of client code is broken. This is unintuitive, but as experienced library implementers will tell you, it's still true. Once you've declared a data member public or protected and clients have started using it, it's very hard to change anything about that data member. Too much code has to be rewritten, retested, redocumented, or recompiled. From an encapsulation point of view, there are really only two access levels: private (which offers encapsulation) and everything else (which doesn't). 比private更彻底的方案是把私有部分放在一个私有的内部类中,并把私有类的定义从公开的头文件中删掉,只被实现所见,这个方案性能损失很小,重构这个类时,不仅不需要修改使用这个类的代码,甚至不需要重新编译使用这个类的代码。
6、Things to Remember
-
Declare data members private. It gives clients syntactically uniform access to data, affords fine-grained access control, allows invariants to be enforced, and offers class authors implementation flexibility.
-
protected is no more encapsulated than public.
<Item 23> Prefer non-member non-friend functions to member functions
7、Object-oriented principles dictate that data and the functions that operate on them should be bundled together, and that suggests that the member function is the better choice. Unfortunately, this suggestion is incorrect. It's based on a misunderstanding of what being object-oriented means. Object-oriented principles dictate that data should be as encapsulated as possible. Counterintuitively, the member function clearEverything actually yields less encapsulation than the non-member clearBrowser. Furthermore, offering the non-member function allows for greater packaging flexibility for WebBrowser-related functionality, and that, in turn, yields fewer compilation dependencies and an increase in WebBrowser extensibility. The non-member approach is thus better than a member function in many ways. It's important to understand why.
class WebBrowser { public: ... void clearCache(); void clearHistory(); void removeCookies(); ... };
//第一种方案 使用成员函数 class WebBrowser { public: ... void clearEverything(); // calls clearCache, clearHistory, // and removeCookies ... };
//另一种方案使用非成员函数 void clearBrowser(WebBrowser& wb) { wb.clearCache(); wb.clearHistory(); wb.removeCookies(); }
8、The greater something is encapsulated, then, the greater our ability to change it. That's the reason we value encapsulation in the first place: it affords us the flexibility to change things in a way that affects only a limited number of clients.
9、Consider the data associated with an object. The less code that can see the data (i.e., access it), the more the data is encapsulated, and the more freely we can change characteristics of an object's data, such as the number of data members, their types, etc. As a coarse-grained measure of how much code can see a piece of data, we can count the number of functions that can access that data: the more functions that can access it, the less encapsulated the data.我们应该精简模块的对外的接口,任何可以从其他接口实现出来的接口都是多余的,至少它们不应该在同一个层次上。
10、At this point, two things are worth noting. First, this reasoning applies only to non-member non-friend functions. Friends have the same access to a class's private members that member functions have, hence the same impact on encapsulation. From an encapsulation point of view, the choice isn't between member and non-member functions, it's between member functions and non-member non-friend functions. (Encapsulation isn't the only point of view, of course. Item 24 explains that when it comes to implicit type conversions, the choice is between member and non-member functions.)
The second thing to note is that just because concerns about encapsulation dictate that a function be a non-member of one class doesn't mean it can't be a member of another class. This may prove a mild salve to programmers accustomed to languages where all functions must be in classes (e.g., Eiffel, Java, C#, etc.). For example, we could make clearBrowser a static member function of some utility class. As long as it's not part of (or a friend of) WebBrowser, it doesn't affect the encapsulation of WebBrowser's private members.
11、In C++, a more natural approach would be to make clearBrowser a non-member function in the same namespace as WebBrowser:
namespace WebBrowserStuff { class WebBrowser { ... }; void clearBrowser(WebBrowser& wb); ... }
12、This has more going for it than naturalness, however, because namespaces, unlike classes, can be spread across multiple source files.A class like WebBrowser might have a large number of convenience functions, some related to bookmarks, others related to printing, still others related to cookie management, etc. As a general rule, most clients will be interested in only some of these sets of convenience functions. There's no reason for a client interested only in bookmark-related convenience functions to be compilation dependent on, e.g., cookie-related convenience functions. The straightforward way to separate them is to declare bookmark-related convenience functions in one header file, cookie-related convenience functions in a different header file, printing-related convenience functions in a third, etc.:
// header "webbrowser.h" — header for class WebBrowser itself // as well as "core" WebBrowser-related functionality namespace WebBrowserStuff { class WebBrowser { ... }; ... // "core" related functionality, e.g. // non-member functions almost // all clients need } // header "webbrowserbookmarks.h" namespace WebBrowserStuff { ... // bookmark-related convenience } // functions // header "webbrowsercookies.h" namespace WebBrowserStuff { ... // cookie-related convenience } // functions ...
Note that this is exactly how the standard C++ library is organized.This allows clients to be compilation dependent only on the parts of the system they actually use. (See Item 31 for a discussion of other ways to reduce compilation dependencies.) Partitioning functionality in this way is not possible when it comes from a class's member functions, because a class must be defined in its entirety; it can't be split into pieces.
13、Putting all convenience functions in multiple header files — but one namespace — also means that clients can easily extend the set of convenience functions. All they have to do is add more non-member non-friend functions to the namespace. For example, if a WebBrowser client decides to write convenience functions related to downloading images, he or she just needs to create a new header file containing the declarations of those functions in the WebBrowserStuff namespace.
14、Things to Remember
-
Prefer non-member non-friend functions to member functions. Doing so increases encapsulation, packaging flexibility, and functional extensibility.
<Item 24> Declare non-member functions when type conversions should apply to all parameters
15、You know that, say, multiplication of rational numbers is related to the Rational class, so it seems natural to implement operator* for rational numbers inside the Rational class.
class Rational { public: Rational(int numerator = 0, // ctor is deliberately not explicit; int denominator = 1); // allows implicit int-to-Rational // conversions int numerator() const; // accessors for numerator and int denominator() const; // denominator — see Item 22 const Rational operator*(const Rational& rhs) const; private: ... }; result = oneHalf * 2; // fine result = 2 * oneHalf; // error!
It turns out that parameters are eligible for implicit type conversion only if they are listed in the parameter list. The implicit parameter corresponding to the object on which the member function is invoked — the one this points to — is never eligible for implicit conversions. That's why the first call compiles and the second one does not. The first case involves a parameter listed in the parameter list, but the second one doesn't.
16、You'd still like to support mixed-mode arithmetic, however, and the way to do it is by now perhaps clear: make operator* a non-member function, thus allowing compilers to perform implicit type conversions on all arguments:
class Rational { ... // contains no operator* }; const Rational operator*(const Rational& lhs, // now a non-member const Rational& rhs) // function { return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); }
Rational oneFourth(1, 4); Rational result; result = oneFourth * 2; // fine result = 2 * oneFourth; // hooray, it works!
17、Should operator* be made a friend of the Rational class?
In this case, the answer is no, because operator* can be implemented entirely in terms of Rational's public interface. The code above shows one way to do it. That leads to an important observation: the opposite of a member function is a non-member function, not a friend function. Too many C++ programmers assume that if a function is related to a class and should not be a member (due, for example, to a need for type conversions on all arguments), it should be a friend. This example demonstrates that such reasoning is flawed. Whenever you can avoid friend functions, you should, because, much as in real life, friends are often more trouble than they're worth. Sometimes friendship is warranted, of course, but the fact remains that just because a function shouldn't be a member doesn't automatically mean it should be a friend.
18、This Item contains the truth and nothing but the truth, but it's not the whole truth. When you cross the line from Object-Oriented C++ into Template C++ (see Item 1) and make Rational a class template instead of a class, there are new issues to consider, new ways to resolve them, and some surprising design implications. Such issues, resolutions, and implications are the topic of Item 46.
19、Things to Remember
-
If you need type conversions on all parameters to a function (including the one pointed to by the this pointer), the function must be a non-member.
<Item 25> Consider support for a non-throwing swap
20、swap is an interesting function. Originally introduced as part of the STL, it's since become a mainstay of exception-safe programming (see Item 29) and a common mechanism for coping with the possibility of assignment to self (see Item 11). 标准库的通常实现如下
namespace std { template<typename T> // typical implementation of std::swap; void swap(T& a, T& b) // swaps a's and b's values { T temp(a); a = b; b = temp; } }
通常自己的类实现赋值函数和复制构造函数,即可以使用标准库中的swap,但是标准库中的swap不一定是最高效的,比如对于 "pimpl idiom" ("pointer to implementation" — see Item 31)这种,只需要交换对应的指针即可
class WidgetImpl { // class for Widget data; public: // details are unimportant ... private: int a, b, c; // possibly lots of data — std::vector<double> v; // expensive to copy! ... }; class Widget { // class using the pimpl idiom public: Widget(const Widget& rhs); Widget& operator=(const Widget& rhs) // to copy a Widget, copy its { // WidgetImpl object. For ... // details on implementing *pImpl = *(rhs.pImpl); // operator= in general, ... // see Items 10, 11, and 12. } ... private: WidgetImpl *pImpl; // ptr to object with this }; // Widget's data
21、对于"pimpl idiom"类,可以通过模版特化做如下实现,但是因为a、b是private变量,因此编译不过!
namespace std { template<> // this is a specialized version void swap<Widget>(Widget& a, // of std::swap for when T is Widget& b) // Widget; this won't compile { swap(a.pImpl, b.pImpl); // to swap Widgets, just swap } // their pImpl pointers }
The "template<>" at the beginning of this function says that this is a total template specialization for std::swap, and the "<Widget>" after the name of the function says that the specialization is for when T is Widget. In other words, when the general swap template is applied to Widgets, this is the implementation that should be used. In general, we're not permitted to alter the contents of the std namespace, but we are allowed to totally specialize standard templates (like swap) for types of our own creation (such as Widget).
22、As I said, though, this function won't compile. That's because it's trying to access the pImpl pointers inside a and b, and they're private. We could declare our specialization a friend, but the convention is different: it's to have Widget declare a public member function called swap that does the actual swapping, then specialize std::swap to call the member function: 这样和STL的实现保持一致,即同时提供了public的swap成员函数和std::swap的特化版本
class Widget { // same as above, except for the public: // addition of the swap mem func ... void swap(Widget& other) { using std::swap; // the need for this declaration // is explained later in this Item swap(pImpl, other.pImpl); // to swap Widgets, swap their } // pImpl pointers ... }; namespace std { template<> // revised specialization of void swap<Widget>(Widget& a, // std::swap Widget& b) { a.swap(b); // to swap Widgets, call their } // swap member function }
23、Widget and WidgetImpl是类模板的时候,情况有些差异了
template<typename T> class WidgetImpl { ... }; template<typename T> class Widget { ... };
下面部分特化的代码是错误的 。We're trying to partially specialize a function template (std::swap), but though C++ allows partial specialization of class templates, it doesn't allow it for function templates. This code should not compile (though some compilers erroneously accept it).
namespace std { template<typename T> // an overloading of std::swap void swap(Widget<T>& a, // (note the lack of "<...>" after Widget<T>& b) // "swap"), but see below for { a.swap(b); } // why this isn't valid code }
When you want to "partially specialize" a function template, the usual approach is to simply add an overload. That would look like this:
namespace std { template<typename T> // an overloading of std::swap void swap(Widget<T>& a, // (note the lack of "<...>" after Widget<T>& b) // "swap"), but see below for { a.swap(b); } // why this isn't valid code }
In general, overloading function templates is fine, but std is a special namespace, and the rules governing it are special, too. It's okay to totally specialize templates in std, but it's not okay to add new templates (or classes or functions or anything else) to std. The contents of std are determined solely by the C++ standardization committee, and we're prohibited from augmenting what they've decided should go there. Alas, the form of the prohibition may dismay you. Programs that cross this line will almost certainly compile and run, but their behavior is undefined. If you want your software to have predictable behavior, you'll not add new things to std.变通方案如下
namespace WidgetStuff { ... // templatized WidgetImpl, etc. template<typename T> // as before, including the swap class Widget { ... }; // member function ... template<typename T> // non-member swap function; void swap(Widget<T>& a, // not part of the std namespace Widget<T>& b) { a.swap(b); } }
Now, if any code anywhere calls swap on two Widget objects, the name lookup rules in C++ (specifically the rules known as argument-dependent lookup or Koenig lookup) will find the Widget-specific version in WidgetStuff. Which is exactly what we want.
24、What you desire is to call a T-specific version if there is one, but to fall back on the general version in std if there's not. Here's how you fulfill your desire:When compilers see the call to swap, they search for the right swap to invoke. C++'s name lookup rules ensure that this will find any T-specific swap at global scope or in the same namespace as the type T. (For example, if T is Widget in the namespace WidgetStuff, compilers will use argument-dependent lookup to find swap in WidgetStuff.) If no T-specific swap exists, compilers will use swap in std, thanks to the using declaration that makes std::swap visible in this function. Even then, however, compilers will prefer a T-specific specialization of std::swap over the general template, so if std::swap has been specialized for T, the specialized version will be used.The one thing you want to be careful of is to not qualify the call, because that will affect how C++ determines the function to invoke(例如std::swap(obj1, obj2);)
template<typename T> void doSomething(T& obj1, T& obj2) { using std::swap; // make std::swap available in this function ... swap(obj1, obj2); // call the best swap for objects of type T ... }
Alas, some misguided programmers do qualify calls to swap in this way, and that's why it's important to totally specialize std::swap for your classes: it makes type-specific swap implementations available to code written in this misguided fashion. (Such code is present in some standard library implementations, so it's in your interest to help such code work as efficiently as possible.)
25、总结下来
- First, if the default implementation of swap offers acceptable efficiency for your class or class template, you don't need to do anything. Anybody trying to swap objects of your type will get the default version, and that will work fine.
- Second, if the default implementation of swap isn't efficient enough (which almost always means that your class or template is using some variation of the pimpl idiom), do the following:
-
Offer a public swap member function that efficiently swaps the value of two objects of your type. For reasons I'll explain in a moment, this function should never throw an exception.
-
Offer a non-member swap in the same namespace as your class or template. Have it call your swap member function.
-
If you're writing a class (not a class template), specialize std::swap for your class. Have it also call your swap member function.
- Finally, if you're calling swap, be sure to include a using declaration to make std::swap visible in your function, then call swap without any namespace qualification.
26、The only loose end is my admonition to have the member version of swap never throw exceptions. That's because one of the most useful applications of swap is to help classes (and class templates) offer the strong exception-safety guarantee. Item 29 provides all the details, but the technique is predicated on the assumption that the member version of swap never throws. This constraint applies only to the member version! It can't apply to the non-member version, because the default version of swap is based on copy construction and copy assignment, and, in general, both of those functions are allowed to throw exceptions. When you write a custom version of swap, then, you are typically offering more than just an efficient way to swap values; you're also offering one that doesn't throw exceptions. As a general rule, these two swap characteristics go hand in hand, because highly efficient swaps are almost always based on operations on built-in types (such as the pointers underlying the pimpl idiom), and operations on built-in types never throw exceptions.
27、Things to Remember
-
Provide a swap member function when std::swap would be inefficient for your type. Make sure your swap doesn't throw exceptions.
-
If you offer a member swap, also offer a non-member swap that calls the member. For classes (not templates), specialize std::swap, too.
-
When calling swap, employ a using declaration for std::swap, then call swap without namespace qualification.
-
It's fine to totally specialize std templates for user-defined types, but never try to add something completely new to std.