Spoiler alert! This is gonna be a long long blog.
Why Write This BLOG
I’ve been meaning to write this blog post—or you could call it a summary—for a very long time. Effective C++ is written and translated by true C++ masters, and this work has enjoyed global renown for many years. But no book is perfect, and no person is flawless. For various reasons (which I’d rather describe as “issues that make me uncomfortable”), I find it necessary to write a summary that serves more like a reference guide version of Effective C++. My hope is that when I want to revisit a particular item in the future, I can save as much time as possible. If anyone who hasn’t read the book gets impatient because of the translation or other factors, you can take a look at this article. From a Chinese (Mainland) perspective and language habit (the original translator is from Taiwan), I’ll try to cover each item’s most important knowledge points as directly and clearly as possible so you can grasp the core ideas in the shortest time possible, then tackle each problem one by one. I don’t think this article can replace Effective C++—it’s far from sufficient. I also won’t include too much code or too many details here. If you want to dig into every detail, pick up the original book and read it, page by page.
First, let me talk about what makes me uncomfortable about this book:
- Some content is a bit outdated. The book doesn’t cover C++11. In other words, with more recent compilers, many items addressing solutions via C++98 feel somewhat redundant. In my summary of each item, I’ll directly point out solutions in newer C++ versions. Personally, I think some methods introduced in the book can be retired. These include but are not limited to
final, override, shared_ptr, = delete
. - The translation is stiff. This isn’t Hou Jie’s fault. Faced with a master’s work, we inevitably vacillate between preserving the original language style and trying to adapt to the language habits of each reader base, which leads to some rather English-flavored expressions appearing in the text, such as “在这个故事结束之前” or “那就进入某某某变奏曲了,” making readers unfamiliar with English feel baffled—“what do they mean by ‘variation’?” Honestly, even I, who know the English original, find it a bit odd. So in my summary, whenever cause and effect are involved, I’ll speak them plainly. Since I’m not a master, I’ll focus on efficiency.
- The author’s writing style requires the reader to treat the book like a novel. In explaining each item, the author carefully prepared all kinds of jokes, famous quotes, historical references, and examples to minimize a stuffy textbook feel and to make the “lecture” less rigid (although the translation mentioned above stiffened it a bit). But on my second and even third reading of the book, I wanted it to be more like a reference guide. For example, if a certain item solves a certain problem, on my first reading I pay attention to how the problem is solved; on my second reading, I may want to know in which situations this type of problem might occur—when to use it—which is my main concern. Unfortunately, the three scenarios that trigger this problem may be scattered across the corners of that item, requiring me to read once again the jokes (now unfunny) and the historical references (no longer interesting) just to gather them properly. Hence this blog post organizes the points that I care about most when reviewing, aiming to let me recall an item’s outline in two minutes or less. That’s the purpose of this post.
Finally, once again, my utmost respect to Meyers and Hou Jie.
II. Construction, Destruction, and Assignment
Construction and destruction, on the one hand, mark the birth and termination of objects; on the other hand, they also mean resource allocation and deallocation. Errors in these operations can lead to far-reaching consequences—you face risks for every object you create and destroy. These functions form the backbone of a custom class. Ensuring their correct behavior is a matter of life and death.
Item 05: Know which functions C++ silently writes and calls
For any class you write, the compiler will actively declare a copy constructor, a copy assignment operator, and a destructor; at the same time, if you don’t declare any constructor, the compiler will also declare a default version of the copy constructor. These functions are all public
and inline
. Note that this is about declarations only. These functions are only implemented by the compiler if they are called. However, the compiler-generated functions can cause problems if there are references or pointers within the class, const
members, or if the type is virtual.
- For the copy constructor, consider whether the class’s members require deep copying. If they do, you need to implement your own copy constructor/operator rather than relying on the compiler.
- For the copy constructor, if the class has reference members or
const
members, you need to define the copy behavior yourself, since the compiler-generated copy logic is likely to be problematic for these two cases. - For the destructor, if the class is meant to be polymorphic, proactively declare the destructor as
virtual
. For details, see Item 07.
Aside from these special cases, if the type is anything more complex than the most trivial kind, write the constructor, destructor, copy constructor, copy assignment operator, move constructor, and move assignment operator (C++11, if necessary) yourself.
Item 06: If you don’t want the compiler to automatically generate a function, explicitly refuse it
Continuing the previous item, if, in terms of semantics or functionality, your type must forbid certain functions (for example, if copying is disallowed), then you should forbid the compiler from automatically generating them. The author provides two ways to achieve this:
- Declare the forbidden functions as
private
and omit their implementations, preventing calls from outside the class. However, if they’re accidentally called inside the class (from a member function or a friend), you’d get a linker error. - Move the potential linker error to compile time by designing a non-copyable utility base class, and let your truly non-copyable class privately inherit from that base. But this approach is overly complex for a type that already uses inheritance, as it introduces multiple inheritance and makes code obscure.
But with C++11, you can simply use = delete
for the copy constructor to explicitly forbid the compiler from generating it.
Item 07: Declare virtual
for polymorphic base classes
The core of this item is: A base class with polymorphic properties must declare its destructor as virtual to prevent only partial destruction of the object when deleting a derived object through a base pointer. If a class entails polymorphism, it’s almost inevitable that a base pointer or reference will point to a derived object. Because non-virtual functions don’t have dynamic types, if the base’s destructor isn’t virtual, when a base pointer is destroyed, it will call only the base destructor, leading to partial destruction of a derived object and the risk of a memory leak. In addition:
- Note that an ordinary base class does not—and should not—have a virtual destructor, because a virtual function implies cost in both time and space. For details, see More Effective C++, Item 24.
- If a type isn’t designed to be a base class but might be inherited from by mistake, declare the class as
final
(C++11) to prohibit derivation and avoid the above problem. - The compiler-generated destructor is non-virtual, so a polymorphic base class must explicitly declare its destructor as
virtual
.
Item 08: Don’t let exceptions escape destructors
Generally speaking, a destructor shouldn’t throw exceptions because that can result in various undefined problems, including but not limited to memory leaks, program crashes, or resource ownership getting stuck.
A straightforward explanation is that a destructor is the last moment of an object’s lifetime and is responsible for handing back important resources such as threads, connections, and memory ownership. If an exception is thrown at some point during the destructor, it means the remaining cleanup code won’t be executed, which is extremely dangerous—because a destructor often acts as the safety net for the class object, possibly called when an exception has already occurred somewhere else. In that scenario, if the destructor is also throwing an exception during stack unwinding, the program may crash immediately, which no programmer wants to see.
That said, if certain operations within a destructor are prone to throwing exceptions (like resource release), and you don’t want to swallow them, then move them out of the destructor and offer a normal function for that cleanup. The destructor should only record some data. We must ensure the destructor can always reach the end without throwing.
Item 09: Never call virtual
functions during construction or destruction
As the item title states: do not call virtual
functions in constructors or destructors.
In a polymorphic context, we need to rethink the meaning of constructors and destructors. During their execution, the object’s type transitions from a base to a derived class and then from a derived class back to a base.
When a derived object begins creation, the base constructor is called first. Until the derived constructor is invoked, the object remains a “base object.” Naturally, any virtual function calls in the base constructor will point to the base version. Once you enter the derived constructor, the original base object becomes a derived object, and calling the virtual function there invokes the derived version. Similarly, during destruction, the derived object type degenerates to the base type.
Therefore, if you’re hoping the base constructor calls a derived virtual function, forget about it. Unfortunately, you may do that unintentionally. For instance, a common practice is to abstract the constructor’s main work into an init()
function to avoid repeating code in multiple constructors, but you need to be careful about whether init()
calls a virtual function. The same holds for the destructor.
Item 10: Have operator=
return a reference to this
Put simply, this enables your assignment operator to support chained assignments:
x = y = z = 10;
When designing interfaces, an important principle is to make them as similar as possible to the built-in types that offer the same functionality. If there’s no special reason, let the return type of your assignment operator be ObjectClass&
and then return *this
in your implementation.
Item 11: Handle “self-assignment” in operator=
Self-assignment refers to assigning something to itself. Although this operation may look silly and useless, it happens far more often than one might think, frequently by way of pointer manipulation:
*pa = *pb; //pa and pb points to the same object, this is self-assignment
arr[i] = arr[j]; //if i == j, this is also self-assignment
Therefore, in the operator=
that manages certain resources, be particularly careful to check whether it’s a self-assignment. Whether you use deep copying or resource ownership transfer, you must release the original memory or ownership before assignment. If you fail to handle self-assignment, you could release your own resource and then assign it back to yourself—an error.
One way is to check for self-assignment before the assignment, but that approach isn’t exception-safe. Imagine if an exception is thrown at any point before assigning but after freeing the original pointer, then the pointer references memory that’s already been deleted.
SomeClass& SomeClass::operator=(const SomeClass& rhs) {
if (this == &rhs) return *this;
delete ptr;
ptr = new DataBlock(*rhs.ptr); //If an exception is thrown out here, ptr will be pointing to memory that has been deleted
return *this;
}
If we also consider exception safety, we arrive at a method that, happily, also solves the self-assignment problem.
SomeClass& SomeClass::operator=(const SomeClass& rhs) {
DataBlock* pOrg = ptr;
ptr = new DataBlock(*rhs.ptr); //If an exception is thrown out here, ptr will still point to the memory before
delete pOrg;
return *this;
}
An alternative using copy-and-swap is explained in detail in Item 29.
Item 12: Don’t forget every component when copying objects
By “every component,” the author is reminding you of two points:
- When you add new member variables to a class, don’t forget to handle them in your copy constructor and assignment operator. If you forget, the compiler won’t warn you.
-
If your class involves inheritance, when implementing a copy constructor for the derived class, be extra mindful to copy every part of the base class. These parts tend to be private, so you can’t directly access them. You should let the derived copy constructor call the base copy constructor:
ChildClass::ChildClass(const ChildClass& rhs) : BaseClass(rhs) { // ... }
Moreover, neither your copy constructor nor your copy assignment operator should call the other. Although it may look like a good way to avoid duplication, it’s flawed. A copy constructor creates a new object (which doesn’t exist before the call), while the assignment operator modifies an existing object (which already exists). The former calling the latter is akin to assigning to an uninitialized object; the latter calling the former is like constructing an object that already exists. Don’t do it!
III. Resource Management
Memory is just one of many resources we manage. The same principle applies to other common resources like mutexes, file descriptors, and database connections: if you’re no longer using them, make sure they’re returned to the system. This chapter discusses handling resource management in the context of exceptions, multiple return paths within functions, and sloppy maintenance by programmers. Besides introducing object-based resource management, it also delves into deeper suggestions for memory management.
Item 13: Manage resources with objects
The core idea of this item is that if you manage resources (acquisition and release) in a process-oriented way, unexpected situations may cause loss of control over those resources, leading to leaks. Process-oriented resource management means encapsulating acquisition and release in separate functions, which forces the caller who acquires resources to be responsible for releasing them. We then have to consider: will the caller always remember to release them? Can they ensure they’re released properly? A design that doesn’t assign too many duties to the caller is a good design.
First, let’s see what might cause the caller’s plan to release resources to fail:
- A simple
delete
statement may not execute if there’s an earlyreturn
or if an exception is thrown before thedelete
. - Even if the code is carefully written at first, when the software is maintained, someone else might add a
return
or throw an exception before thedelete
, repeating the same error.
To ensure resources are acquired and released properly, we package their acquisition and release into an object. When we construct the object, it automatically acquires the resource. When we no longer need that resource, we let the object’s destructor handle it. That’s the idea behind “Resource Acquisition Is Initialization (RAII),” because we always initialize a management object in the same statement that acquires the resource. No matter how control flow exits that block, once the object is destroyed (for instance, when leaving its scope), its destructor is automatically invoked.
For a practical example in C++11, see shared_ptr<T>
.
IV. Design and Declarations
Interface design and declarations are an entire discipline in themselves. Note that I’m talking about how the interface should look, not the internal implementation. How should you choose parameter types? What about return types? Should a function go inside the class or outside of it? These decisions profoundly impact interface stability and correctness. This chapter addresses these concerns one by one.
Item 18: Make interfaces easy to use correctly and hard to use incorrectly
This item tells you how to help your clients avoid mistakes when using your interface.
When designing interfaces, we often wrongly assume that interface users possess some necessary knowledge to avoid obvious mistakes. But the fact is, they might not be as “clever” as we are or know the “inside information” of the interface’s implementation, and that leads to instability. Such instability might be due to the caller lacking prior knowledge or simply being careless. The caller might be someone else or your future self. Therefore, a well-designed interface should, as much as possible, help its callers avoid potential hazards at the syntax level and before the program runs, i.e., at compile time.
- Use wrapper types to remind callers to verify their parameters, restricting additional conditions to the type itself
When someone tries to pass “13” as a “month,” you could check at runtime inside the function and issue a warning or throw an exception, but that’s just shifting the blame—only after calling does the user discover they mistakenly typed 13 instead of 12. If you abstract “month” into a separate type at the design level (for example, using an enum class), you can catch the problem at compile time. Restricting a parameter’s additional conditions to the type itself helps make the interface more convenient.
- Restrict what callers cannot do at the syntax level
Callers often make mistakes unknowingly. Therefore, you need to impose constraints at the syntax level. A common example is adding const
to a function’s return type—for instance, having the return type of operator*
be const
to prevent an accidental assignment like if (a * b = c)
.
- Make your interface behave consistently with built-in types
Let your custom type behave like built-in types. For example, if you design your own container, match the naming convention of the STL. Or if you need two objects to be multiplied, it’s better to overload operator*
rather than create a member function called “multiply.”
- At the syntax level, require what callers must do
Never rely on callers to remember to do certain tasks. The interface designer should assume they’ll forget those requirements. For example, replacing raw pointers with smart pointers is a way of being mindful of the caller. If a core method needs setup and teardown (like acquiring a lock and releasing it) before and after use, it might be better to define pure virtual functions and force the caller to inherit from an abstract class, guaranteeing they implement those steps. The interface designer is responsible for calling those setup and teardown steps around the core method call.
The fewer responsibilities the caller (our client) has, the fewer mistakes they can make.
Item 19: Design a class as you would design a type
This item reminds us that designing a class requires attention to detail, though it doesn’t provide solutions to all of them—it’s just a reminder. Each time you design a class, mentally walk through these questions:
- How should objects be created and destroyed? Consider constructors, destructors, and possibly overriding
new
/delete
. - How should the constructor differ from the assignment operator in behavior, especially regarding resource management?
- What happens if the object is copied? Consider a copy constructor.
- What are valid values for the object? Ideally, ensure this at the syntax level or at least before runtime.
- Should the new type fit into some existing inheritance system? That might involve virtual function overrides.
- What about implicit conversions between the new type and existing types? That implies thinking about conversion operators and non-
explicit
constructors. - Should any operators be overloaded?
- Which interface should be exposed, and which techniques should be encapsulated (public vs. private)?
- What about efficiency, resource management, thread safety, and exception safety?
- Does this class have the potential to be a template? If so, consider making it a template class.
Item 20: Prefer pass-by-reference-to-const over pass-by-value
When designing a function’s interface, you should usually take parameters by const
reference rather than by value, or you risk the following:
- Passing by value can involve copying large amounts of data, and many of these copies are unnecessary.
- If the copy constructor is designed for deep copying rather than shallow copying, the copying cost may far exceed just copying a few pointers.
- In a polymorphic context, if you pass a derived object to a function that expects a base by value, only the base portion is copied, discarding the derived part. That can cause unpredictable errors, and virtual functions won’t be called.
- Even if a type is small, that doesn’t guarantee that pass-by-value is cheap. The type’s size depends significantly on the compiler. Furthermore, small can become large in future code reuse or refactoring.
Nonetheless, for built-in types or STL iterators and function objects, we usually stick to pass-by-value.
Item 21: If you must return an object, don’t try returning its reference
The core message of this item is not to return the result by reference. The author analyzes the many potential errors, be it returning a stack object or a heap object. It won’t be repeated here. The author’s final conclusion: if you must return by value, just do it. That extra copy isn’t a big deal, and you can rely on compiler optimizations.
However, with C++11 or later compilers, you can write a “move constructor” for your type and use std::move()
to elegantly eliminate the time and space overhead caused by copying.
Item 22: Declare data members as private
First, the conclusion—declare all data members in a class as private
. private
implies variable encapsulation. But this item contributes more valuable insights on how different access specifiers—public
, private
, protected
—reflect design philosophies.
Put simply, making all members private has two benefits. First, all the public
and protected
members are functions, so the user no longer needs to distinguish among them, ensuring syntactic consistency. Second, by encapsulating the variables, you minimize the necessary changes in external code if you alter the class internally.
Once all variables are encapsulated, no external entity can access them directly. If users (potentially your future self or someone else) want to do something with these private variables, they must do so via the interfaces you provide. These interfaces buffer outside code from the class’s internal changes—what’s invisible causes no impact—thus not forcing outside code to change. So if a well-designed class is changed internally, the impact on the rest of the project should merely be a recompile rather than code modifications.
Next, public
and protected
are partially equivalent. A custom type is offered to its “clients,” who typically use it in one of two ways—they either instantiate it or inherit from it—we’ll call these two categories “the first kind of client” and “the second kind of client.” From an encapsulation standpoint, a public
member is one that the class author decides not to encapsulate from the first kind of client, and a protected
member is one that the class author decides not to encapsulate from the second kind of client. In other words, if we treat both client types equally, then public
, protected
, and private
reflect how the class designer handles encapsulation—full, partial, or none.
Item 23: Prefer non-member, non-friend to member functions
I’m willing to elaborate more on this item because it’s very important, and the author didn’t explain it as clearly as one might hope.
Within a class, I would describe those public
or protected
member functions that need direct access to private members as low-granularity functions. They form the first line of encapsulation for private members. By contrast, public member functions that are formed by combining several other public (or protected) functions I’d call high-granularity functions. These high-granularity functions don’t need direct access to private members themselves—they merely piece together lower-level tasks. This item tells us that such functions should, whenever possible, be placed outside the class.
class WebBrowser { // We browser class
public:ß
void clearCache(); // Clear caches, this has direct access to private member
void clearHistory(); // Clear caches, this has direct access to private member
void clearCookies(); // Clear cookies, this has direct access to private member
void clear(); // This function has larger granularity, and calls the three functions above. Therefore, it should not have direct access to private members. This Clause tells us to move this function outside of the class.
}
If these high-granularity functions remain as member functions in the class, they can, on the one hand, erode encapsulation and, on the other, reduce the flexibility of how the function might be wrapped.
- Class encapsulation
Encapsulation’s purpose is to minimize the impact of internal changes on outside code—we hope only a small number of clients are affected by changes inside the class. A simple method to measure the encapsulation quality of a member is to see how many public
or protected
functions directly access that member. The more that do, the weaker the member’s encapsulation—its changes could spread further. Back to our discussion: high-granularity functions, at the time of design, are not supposed to directly access any private members but instead use public members. That’s the best way to maintain encapsulation. Unfortunately, this intention isn’t enforced in code. A future maintainer (maybe you or someone else) might forget and directly access private members in what was supposed to be a “high-granularity” function, accidentally damaging encapsulation. By making it a non-member function, you avoid that possibility at the syntax level.
- Wrapping flexibility and design approach
Extracting high-granularity functions to the outside allows us to organize code from more perspectives and optimize compile dependencies. For instance, if the function clear()
is initially created to integrate low-granularity tasks from the browser’s perspective, you might reorganize those tasks from the perspectives of “cache,” “history,” “cookies,” etc. Perhaps you combine “search history” and “clear history” into “selective clear history,” or “export cache” and “clear cache” into “export and clear cache.” Doing that outside the browser class yields greater flexibility. Usually, one might use a utility class like class CacheUtils
or class HistoryUtils
with static functions, or place them in separate namespaces. Then if you only need certain functionality, you can include the relevant header, rather than forcibly bringing in code for cookies when you only care about cache. That’s also the benefit of namespaces being able to span multiple files.
// Header file webbrowser.h: For class WebBrowserStuff itself
namespace WebBrowserStuff {
class WebBrowser { ... }; // Core functionalities
}
// Header file webbrowsercookies.h: For WebBrowser and cookie-related functionalities
namespace WebBrowserStuff {
... // Utility functions related to cookies
}
// Header file webbrowsercache.h: For WebBrowser and cache-related functionalities
namespace WebBrowserStuff {
... // Utility functions related to cache
}
Finally, note that this item refers to functions that do not directly access private members. If you have a public
(or protected
) function that must directly touch private members, forget this item, because extracting that function to the outside would be far more involved.
Item 24: If all parameters need type conversion, use a non-member function
This item addresses the difference between overloading an operator as a member function versus a non-member function. The author wants to point out that if you expect every operand to support implicit conversions for the operator, then make that operator a non-member.
First, note that if an operator is a member function, then its first operand (the calling object) does not undergo implicit conversions.
Let’s begin with a brief explanation: once an operator is written as a member function, it becomes less obvious from the expression which object is actually calling it. For example, if a rational number class overloads the +
operator, and you use Rational z = x + y;
, you don’t see which object is really calling operator+
—does the this
pointer refer to x
or y
?
class Rational {
public:
//...
Rational operator+(const Rational rhs) const;
pricate:
//...
}
As a member function, the operator’s invisible this
pointer always points to the first operand, so Rational z = x.operator+(y);
is effectively what happens. The compiler decides which operator function to call based on the static type of the first operand. Therefore, the first operand can’t be implicitly converted to the correct type. For example, if Rational
’s constructor allows an int
to be implicitly converted to Rational
, then Rational z = x + 2;
compiles because x
is a Rational
and 2
can be converted. But Rational z = 2 + x;
might fail to compile because the first operand is 2
(an int
), so the compiler tries to convert x
to an int
, which doesn’t work.
Hence, if you’re writing operators like addition, subtraction, multiplication, division, etc. (not limited to these) and want each operand to allow implicit conversion, do not overload them as member functions. If the first operand isn’t of the correct type, you’ll run into a failed call. The solution is to declare the operator as a non-member function; you can make it a friend if that makes the operator’s work easier, or you can wrap private members behind more public functions—whatever you choose.
Hopefully, this clarifies why operators behave differently when written as member functions versus non-members. The rule doesn’t strictly apply only to operators, but other than operators, it’s hard to think of a more suitable example.
By the way, if you want to forbid implicit conversions, mark every single-parameter constructor with the explicit
keyword.
Item 25: Consider writing a non-throwing swap
function
VI. Inheritance and Object-Oriented Design
When designing a class involving inheritance, there are many considerations:
- What type of inheritance is it?
- Are its interfaces virtual or non-virtual?
- How are default parameters handled?
Answering these questions properly demands understanding even more topics: what exactly does each type of inheritance mean? What is the true purpose of a virtual function? How does inheritance affect name lookups? Is a virtual function really necessary? Are there alternatives? These issues are all discussed in this chapter.
Item 32: Ensure your public inheritance represents an is-a relationship
Public inheritance means: the derived class is a specialized version of the base class. That’s the so-called “is-a” relationship. But this item points out a deeper meaning: with public inheritance, the derived class must encompass all features of the base class, unconditionally inheriting all of its traits and interfaces. That’s singled out because if we rely purely on real-world intuition, we might slip up.
For example, is an ostrich a bird? If we consider flight as a feature (or interface), then the Ostrich class definitely can’t publicly inherit from Bird because ostriches can’t fly; we want to eliminate the possibility of calling a flight interface at compile time. But if we only care about laying eggs, then by that logic, an ostrich can indeed inherit from Bird. The same idea applies to rectangles and squares in geometry. Real-world experience says a square is a rectangle, but in code, a rectangle has length and width as two separate variables; a square cannot have two unconstrained variables—there’s no syntax-level way to ensure they’re always equal. Hence, no public inheritance.
So before deciding on public inheritance, first ask yourself, does the derived class need all of the base class’s features? If not, no matter what real life might suggest, it isn’t an “is-a” relationship. Public inheritance won’t reduce or weaken the base class’s traits or interfaces—it can only extend them.
Item 33: Avoid hiding inherited names
This item discusses name hiding among repeatedly overloaded virtual functions in inheritance. If you don’t have a design involving multiple overloads of the same virtual function, you can skip this.
Suppose the base class has two overloads for a virtual function foo()
, maybe foo(int)
and foo() const
. If the derived class overrides only one of them, then the other overloads (foo(int)
and foo() const
) in the base class become unavailable in the derived class. This is the name hiding problem—name hiding at the scope level is independent of parameter types and virtual-ness. Even if the derived class overrides just one function with the same name, all the same-named functions in the base class are effectively hidden. Personally, I find that rather counterintuitive.
If you want to restore the base class’s function names, you must use using Base::foo;
in the scope where you need it in the derived class (perhaps in a member function or under public or private). That will make foo(int)
and foo() const
from the base class visible again in the derived class.
If you only want to reuse one of the base class’s once-hidden overloads in the derived class, you can do so with an inline forwarding function.
Item 34: Distinguish interface inheritance from implementation inheritance
We explored the essential meaning of public inheritance in Item 32. Now, in this item, we clarify that within a public inheritance hierarchy, different types of functions—pure virtual, virtual, and non-virtual—have hidden design logic.
First, you must understand that member function interfaces are always inherited. Public inheritance ensures that if you can call some function on the base class, you can call it on the derived class. Different types of functions reflect the base class’s different expectations of how the derived class will implement them.
- Declaring a pure virtual function in the base class forces the derived class to have that interface and forces it to provide an implementation.
- Declaring a normal virtual function in the base class forces the derived class to have that interface and provides a default implementation for it.
- Declaring a non-virtual function in the base class forces the derived class to accept both the interface and the provided implementation, disallowing any changes by the derived class (Item 36 requires that we not override the base’s non-virtual functions).
The potential issue arises with normal virtual functions, because the default implementation in the base class may not be suitable for all derived classes. Thus, if a derived class forgets to implement a custom version when it should, the base class lacks a mechanism to warn the derived class’s designer at the code level. One solution is to provide an implementation in the base class for the pure virtual function so that if the derived class finds it suitable, it can call that default. This makes the derived class explicitly check the default behavior’s suitability in code.
Hence, the difference between pure virtual and normal virtual functions isn’t whether the base class has an implementation—pure virtual functions can also have one. Instead, it’s that the base class’s expectations differ. The former states, “implement me explicitly,” while the latter states, “use the default or override me if needed.” Non-virtual functions don’t allow any freedom in derived classes, guaranteeing a single consistent implementation across the hierarchy.
Item 35: Consider alternatives to virtual functions
Item 36: Never redefine non-virtual functions inherited from a base class
That means if your function needs dynamic (polymorphic) dispatch, be sure to declare it virtual. Otherwise, if the base function is non-virtual and you “override” it in the derived class, any dynamic calls (through a base pointer to a derived object) won’t call your overridden function, which probably causes errors.
Conversely, if a function in the base class is non-virtual, never override it in the derived class. You’d face a similar problem.
Why? Because only virtual functions are dynamically bound. Non-virtual functions are statically bound at compile time based on the pointer or reference type, ignoring the object’s dynamic type.
In other words, a virtual function means “the interface is indeed inherited, but the implementation can be changed in the derived class,” while a non-virtual function means “both the interface and the implementation are inherited.” That’s the essence of “virtual.”
Item 37: Never redefine inherited default parameter values
This item has two implications:
- Do not alter default parameter values of a non-virtual function in the base class. Essentially, don’t override anything about a non-virtual function in the base. Don’t modify it!
- Virtual functions should not have default parameter values, nor should they be changed in derived classes. A virtual function should always avoid default parameters.
The first point was explained in Item 36. The second is because default parameters belong to static binding, while virtual functions are dynamically bound. Virtual functions are typically used in dynamic calls, but any default parameter values changed in the derived class won’t take effect in that dynamic context, causing confusion and leading the caller to believe they’ve changed while in reality they haven’t.
Default parameter values are statically bound for efficiency reasons at runtime.
If you really want a virtual function to have default parameters in a particular class, make that virtual function private
, and in the public
interface create a non-virtual “wrapper” function that has default parameters. Of course, that wrapper is single-use—don’t override it after inheriting again.
Item 38: Use composition to model has-a or “implemented in terms of”
Besides inheritance, there’s another relationship between two classes: one class’s object can serve as a member of another class. We call this relationship “class composition.” This item explains when composition is appropriate.
The first scenario is quite straightforward: it explains that one class “owns” another class object as an attribute. For instance, a student has a pencil, a citizen has an ID card, etc. No problem there.
The second scenario is more discussed: “one class is implemented in terms of another.” For example, implementing a queue using a stack, or implementing a Redcore browser using an old version of the Google Chrome kernel.
Here, it’s crucial to distinguish the second scenario from the “is-a” relationship in public inheritance. Always remember: the only test for “is-a” is whether a derived class must fully inherit every feature and interface of the base class. Meanwhile, “implemented in terms of another class” is about hiding that tool class. For example, people don’t really care whether your queue is implemented with a stack, so you hide the stack interface and only expose the queue interface. Likewise, Redcore browser developers don’t want others to see that it’s built on Chrome’s kernel, so they need “hidden” behavior.
Item 39: Use private inheritance judiciously and cautiously
Similar to composition, private inheritance expresses “implementing one class by means of another tool class.” By definition, the tool class should be hidden inside the target class—no interfaces or variables are externally exposed. This is the essence of private inheritance: a technical form of encapsulation. Unlike public inheritance, which expresses “both the implementation and the interface are inherited,” private inheritance conveys “only the implementation is inherited, but the interface is omitted.”
Accordingly, in private inheritance, all base class members become private to the derived class. They’re not publicly accessible, and the outside world doesn’t care about the details of the base class that the derived class used.
When you need to “implement one class in terms of another,” how do you decide between composition and private inheritance?
- Prefer composition whenever you can. Avoid private inheritance unless necessary.
- When you need to override certain methods (virtual functions) of the tool class—methods specifically designed for inheritance or callbacks—private inheritance is a better fit, because the user can customize the implementation.
If you do use private inheritance, you can’t prevent further derived classes from overriding the same virtual functions again. If you want to block that behavior—similar to marking a function as final
—one approach is to declare a private nested class inside the target class. That nested class publicly inherits from the tool class and overrides its methods within itself.
class TargetClass { // Target class
private:
class ToolHelperClass : public ToolClass { // Nested class, publicly inheriting from the tool class
public:
void someMethod() override; // Methods that should be overridden by TargetClass are implemented in the nested class, preventing TargetClass's subclasses from overriding them.
};
};
That way, subclasses of your target class can’t re-override those critical methods.
Item 40: Use multiple inheritance judiciously and cautiously
In principle, multiple inheritance isn’t encouraged because it may lead to multiple parents sharing an ancestor, potentially causing the derived class to hold multiple copies of that ancestor. C++ addresses this with virtual inheritance, but that expands object size and slows member data access—both costs of virtual inheritance.
If you must use multiple inheritance, design your classes carefully, avoiding diamond patterns wherever possible (“B and C derive from A, then D derives from both B and C”). If you can’t avoid a diamond pattern, consider using virtual inheritance, but keep in mind the overhead. If the base class is virtually inherited, try to store as little data there as possible.
Spoiler alert! This is gonna be a long long blog.
为什么写这篇BLOG
我动手写这篇博文——或者说总结——的想法已经很久了,《Effective C++》这本书的作者和译者都是C++大师,这篇著作有也已享誉全球很多年。但是书无完书、人无完人,这本书也因为这样或那样的原因(我更愿称之为引起我不适的问题)让我有必要为此写一篇总结,使得这篇总结更像《Effective C++》对应的工具书版本,帮助我在未来想要回顾某一条款的内容时,最大限度地节约我的时间。如果没有读过这本书的读者因为翻译或者是其他问题没有耐心再读下去的时候,不妨也看看这篇文章,我会从一个中国人的逻辑角度,使用大陆人的语言习惯(原译者是中国台湾同胞),尽可能直接并清楚得涵盖每一个条款最重要的知识点,让你在最短的时间抓住核心,再逐个击破各个问题。我并不觉得这篇文章可以就此替代《Effective C++》,其实是远远不够,我并不会在文章中涵盖太多的代码和细节,如果你想要探究每一个细节,请拿起原著,乖乖把每一页看完。
首先,我想说一说这本书让我不适的地方:
- 内容有点老旧。这本书没有涵盖C++11,可以说,有了更高版本的编译器,许多条款使用C++98解决问题的思路和方式都显得有些冗余了,我会在每一条款的总结中直接指出在更高版本C++下的解决方案,个人看来,书中提出的解决问题的方法就可以淘汰了,这些地方包括但不限于
final, override, shared_ptr, = delete
。 - 翻译僵硬。这并不能怪侯捷,因为面对一个大师的作品,我们肯定要在保留语言的原汁原味和尽量符合各国读者的语言风格面前摇摆取舍,但这也造成了相当英文的表达出现在了文中,比如“在这个故事结束之前”,“那就进入某某某变奏曲了”,让不太熟悉英文的读者感到莫名其妙——”变奏曲在哪?“。说实在话,就是知道英文原文的我读起这样的翻译也觉得怪怪的。因此在我的总结中,面对各种因果关系我会把舌头捋直了说,毕竟我不是大师,只注重效率就可以了。
- 作者的行文之风让读者必须以读一本小说的心态去拜读这部著作。在了解每一个条款时,作者精心准备了各种玩笑、名人名言、典故以及例子,尽量让你感觉不到教科书般的迂腐之气,也用了俏皮的语言使授课不那么僵硬(尽管上述翻译还是让它僵硬了起来)。但对于第二甚至是第三次读这本书的我来说,我更希望这本书像一本工具书。例如某条款解决了一个问题,在第一遍读的时候我重点去体会解决问题的方法是什么,第二遍我可能更想知道这种问题在什么情况下可能会发生——什么时候去用,这是我最关心的。不幸的是,出现这一问题的三个场景可能分布在这个条款的角角落落,我必须重新去读一遍那些已经不好笑的笑话、已经不经典的典故,才能把他们整理好。所以,这篇博文替我把上述我在翻阅时更care的内容总结起来,争取两分钟可以把一个条款的纲要回忆起来,这便是这个博文的目的。
最后,再次向Meyers和侯捷大师致敬。
二、构造、析构和赋值运算
构造和析构一方面是对象的诞生和终结;另一方面,它们也意味着资源的开辟和归还。这些操作犯错误会导致深远的后果——你需要产生和销毁的每一个对象都面临着风险。这些函数形成了一个自定义类的脊柱,所以如何确保这些函数的行为正确是“生死攸关”的大事。
条款05:了解C++默默编写并调用了哪些函数
编译器会主动为你编写的任何类声明一个拷贝构造函数、拷贝复制操作符和一个析构函数,同时如果你没有生命任何构造函数,编译器也会为你声明一个default版本的拷贝构造函数,这些函数都是public
且inline
的。注意,上边说的是声明哦,只有当这些函数有调用需求的时候,编译器才会帮你去实现它们。但是编译器替你实现的函数可能在类内引用、类内指针、有const
成员以及类型有虚属性的情形下会出问题。
- 对于拷贝构造函数,你要考虑到类内成员有没有深拷贝的需求,如果有的话就需要自己编写拷贝构造函数/操作符,而不是把这件事情交给编译器来做。
- 对于拷贝构造函数,如果类内有引用成员或
const
成员,你需要自己定义拷贝行为,因为编译器替你实现的拷贝行为在上述两个场景很有可能是有问题的。 - 对于析构函数,如果该类有多态需求,请主动将析构函数声明为
virtual
,具体请看条款07 。
除了这些特殊的场景以外,如果不是及其简单的类型,请自己编写好构造、析构、拷贝构造和赋值操作符、移动构造和赋值操作符(C++11、如有必要)这六个函数。
条款06:若不想使用编译器自动生成的函数,就该明确拒绝。
承接上一条款,如果你的类型在语义或功能上需要明确禁止某些函数的调用行为,比如禁止拷贝行为,那么你就应该禁止编译器去自动生成它。作者在这里给出了两种方案来实现这一目标:
- 将被禁止生成的函数声明为
private
并省略实现,这样可以禁止来自类外的调用。但是如果类内不小心调用了(成员函数、友元),那么会得到一个链接错误。 - 将上述的可能的链接错误转移到编译期间。设计一不可拷贝的工具基类,将真正不可拷贝的基类私有继承该基类型即可,但是这样的做法过于复杂,对于已经有继承关系的类型会引入多继承,同时让代码晦涩难懂。
但是有了C++11,我们可以直接使用= delete
来声明拷贝构造函数,显示禁止编译器生成该函数。
条款07:为多态基类声明virtual
该条款的核心内容为:带有多态性质的基类必须将析构函数声明为虚函数,防止指向子类的基类指针在被释放时只局部销毁了该对象。如果一个类有多态的内涵,那么几乎不可避免的会有基类的指针(或引用)指向子类对象,因为非虚函数没有动态类型,所以如果基类的析构函数不是虚函数,那么在基类指针析构时会直接调用基类的析构函数,造成子类对象仅仅析构了基类的那一部分,有内存泄漏的风险。除此之外,还需注意:
-
需要注意的是,普通的基类无需也不应该有虚析构函数,因为虚函数无论在时间还是空间上都会有代价,详情《More Effective C++》条款24。
- 如果一个类型没有被设计成基类,又有被误继承的风险,请在类中声明为
final
(C++ 11),这样禁止派生可以防止误继承造成上述问题。 - 编译器自动生成的析构函数时非虚的,所以多态基类必须将析构函数显示声明为
virtual
。
条款08:别让异常逃离析构函数
析构函数一般情况下不应抛出异常,因为很大可能发生各种未定义的问题,包括但不限于内存泄露、程序异常崩溃、所有权被锁死等。
一个直观的解释:析构函数是一个对象生存期的最后一刻,负责许多重要的工作,如线程,连接和内存等各种资源所有权的归还。如果析构函数执行期间某个时刻抛出了异常,就说明抛出异常后的代码无法再继续执行,这是一个非常危险的举动——因为析构函数往往是为类对象兜底的,甚至是在该对象其他地方出现任何异常的时候,析构函数也有可能会被调用来给程序擦屁股。在上述场景中,如果在一个异常环境中执行的析构函数又抛出了异常,很有可能会让程序直接崩溃,这是每一个程序员都不想看到的。
话说回来,如果某些操作真的很容易抛出异常,如资源的归还等,并且你又不想把异常吞掉,那么就请把这些操作移到析构函数之外,提供一个普通函数做类似的清理工作,在析构函数中只负责记录,我们需要时刻保证析构函数能够执行到底。
条款09:绝不在构造和析构过程中调用virtual
函数。
结论正如该条款的名字:请不要在构造函数和析构函数中调用virtual
函数。
在多态环境中,我们需要重新理解构造函数和析构函数的意义,这两个函数在执行过程中,涉及到了对象类型从基类到子类,再从子类到基类的转变。
一个子类对象开始创建时,首先调用的是基类的构造函数,在调用子类构造函数之前,该对象将一直保持着“基类对象”的身份而存在,自然在基类的构造函数中调用的虚函数——将会是基类的虚函数版本,在子类的构造函数中,原先的基类对象变成了子类对象,这时子类构造函数里调用的是子类的虚函数版本。这是一件有意思的事情,这说明在构造函数中虚函数并不是虚函数,在不同的构造函数中,调用的虚函数版本并不同,因为随着不同层级的构造函数调用时,对象的类型在实时变化。那么相似的,析构函数在调用的过程中,子类对象的类型从子类退化到基类。
因此,如果你指望在基类的构造函数中调用子类的虚函数,那就趁早打消这个想法好了。但很遗憾的是,你可能并没有意识到自己做出了这样的设计,例如将构造函数的主要工作抽象成一个init()
函数以防止不同构造函数的代码重复是一个很常见的做法,但是在init()
函数中是否调用了虚函数,就要好好注意一下了,同样的情况在析构函数中也是一样。
条款10:令operator =返回一个reference to this
简单来说:这样做可以让你的赋值操作符实现“连等”的效果:
x = y = z = 10;
在设计接口时一个重要的原则是,让自己的接口和内置类型相同功能的接口尽可能相似,所以如果没有特殊情况,就请让你的赋值操作符的返回类型为ObjectClass&
类型并在代码中返回*this
吧。
条款11:在operator=中处理“自我赋值”
自我赋值指的是将自己赋给自己。这是一种看似愚蠢无用但却在代码中出现次数比任何人想象的多得多的操作,这种操作常常需要假借指针来实现:
*pa = *pb; //pa和pb指向同一对象,便是自我赋值。
arr[i] = arr[j]; //i和j相等,便是自我赋值
那么对于管理一定资源的对象重载的operator = 中,一定要对是不是自我赋值格外小心并且增加预判,因为无论是深拷贝还是资源所有权的转移,原先的内存或所有权一定会被清空才能被赋值,如果不加处理,这套逻辑被用在自我赋值上会发生——先把自己的资源给释放掉了,然后又把以释放掉的资源赋给了自己——出错了。
第一种做法是在赋值前增加预判,但是这种做法没有异常安全性,试想如果在删除掉原指针指向的内存后,在赋值之前任何一处跑出了异常,那么原指针就指向了一块已经被删除的内存。
SomeClass& SomeClass::operator=(const SomeClass& rhs) {
if (this == &rhs) return *this;
delete ptr;
ptr = new DataBlock(*rhs.ptr); //如果此处抛出异常,ptr将指向一块已经被删除的内存。
return *this;
}
如果我们把异常安全性也考虑在内,那么我们就会得到如下方法,令人欣慰的是这个方法也解决了自我赋值的问题。
SomeClass& SomeClass::operator=(const SomeClass& rhs) {
DataBlock* pOrg = ptr;
ptr = new DataBlock(*rhs.ptr); //如果此处抛出异常,ptr仍然指向之前的内存。
delete pOrg;
return *this;
}
另一个使用copy and swap技术的替代方案将在条款29中作出详细解释。
条款12:复制对象时勿忘其每一个成分
所谓“每一个成分”,作者在这里其实想要提醒大家两点:
-
当你给类多加了成员变量时,请不要忘记在拷贝构造函数和赋值操作符中对新加的成员变量进行处理。如果你忘记处理,编译器也不会报错。
-
如果你的类有继承,那么在你为子类编写拷贝构造函数时一定要格外小心复制基类的每一个成分,这些成分往往是private的,所以你无法访问它们,你应该让子类使用子类的拷贝构造函数去调用相应基类的拷贝构造函数:
//在成员初始化列表显示调用基类的拷贝构造函数 ChildClass::ChildClass(const ChildClass& rhs) : BaseClass(rhs) { // ... }
除此之外,拷贝构造函数和拷贝赋值操作符,他们两个中任意一个不要去调用另一个,这虽然看上去是一个避免代码重复好方法,但是是荒谬的。其根本原因在于拷贝构造函数在构造一个对象——这个对象在调用之前并不存在;而赋值操作符在改变一个对象——这个对象是已经构造好了的。因此前者调用后者是在给一个还未构造好的对象赋值;而后者调用前者就像是在构造一个已经存在了的对象。不要这么做!
三、资源管理
内存只是众多被管理的资源之一,对待其他常见的资源如互斥锁、文件描述器、数据库连接等时,我们要遵循同一原则——如果你不再使用它们,确保将他们还给系统。本章正是在考虑异常、函数内多重回传路径、程序员不当维护软件的背景下尝试和资源管理打交道。本章除了介绍基于对象的资源管理办法,也专门对内存管理提出了更深层次的建议。
条款13:以对象管理资源
本条款的核心观点在于:以面向流程的方式管理资源(的获取和释放),总是会在各种意外出现时,丢失对资源的控制权并造成资源泄露。以面向过程的方式管理资源意味着,资源的获取和释放都分别被封装在函数中。这种管理方式意味着资源的索取者肩负着释放它的责任,但此时我们就要考虑一下以下几个问题:调用者是否总是会记得释放呢?调用者是否有能力保证合理地释放资源呢?不给调用者过多义务的设计才是一个良好的设计。
首先我们看一下哪些问题会让调用者释放资源的计划付诸东流:
- 一句简单的
delete
语句并不会一定执行,例如一个过早的return
语句或是在delete
语句之前某个语句抛出了异常。 - 谨慎的编码可能能在这一时刻保证程序不犯错误,但无法保证软件接受维护时,其他人在delete语句之前加入的return语句或异常重复第一条错误。
为了保证资源的获取和释放一定会合理执行,我们把获取资源和释放资源的任务封装在一个对象中。当我们构造这个对象时资源自动获取,当我们不需要资源时,我们让对象析构。这便是“Resource Acquisition Is Initialization; RAII”的想法,因为我们总是在获得一笔资源后于同一语句内初始化某个管理对象。无论控制流如何离开区块,一旦对象被销毁(比如离开对象的作用域)其析构函数会自动被调用。
具体实践请参考C++11的shared_ptr<T>
。
四、设计与声明
接口的设计与声明是一门学位,注意我说的是接口的设计——接口长什么样子,而不是接口内部是怎么实现的。接口参数的类型选择有什么学问?接口的返回类型又有什么要注意的地方?接口应该放在类内部还是类的外部?这些问题的答案都将对接口的稳定性、功能的正确性产生深远的影响。在这一章,我们将逐一讨论这些问题。
条款18:让接口容易被正确使用,不易误使用
本条款告教你如何帮助你的客户在使用你的接口时避免他们犯错误。
在设计接口时,我们常常会错误地假设,接口的调用者拥有某些必要的知识来规避一些常识性的错误。但事实上,接口的调用者并不总是像正在设计接口的我们一样“聪明”或者知道接口实现的”内幕信息“,结果就是,我们错误的假设使接口表现得不稳定。这些不稳定因素可能是由于调用者缺乏某些先验知识,也有可能仅仅是代码上的粗心错误。接口的调用者可能是别人,也可能是未来的你。所以一个合理的接口,应该尽可能的从语法层面并在编译之时运行之前,帮助接口的调用者规避可能的风险。
- 使用外覆类型(wrapper)提醒调用者传参错误检查,将参数的附加条件限制在类型本身
当调用者试图传入数字“13”来表达一个“月份”的时候,你可以在函数内部做运行期的检查,然后提出报警或一个异常,但这样的做法更像是一种责任转嫁——调用者只有在尝试过后才发现自己手残把“12”写成了“13”。如果在设计参数类型时就把“月份”这一类型抽象出来,比如使用enum class(强枚举类型),就能帮助客户在编译时期就发现问题,把参数的附加条件限制在类型本身,可以让接口更易用。
- 从语法层面限制调用者不能做的事
接口的调用者往往无意甚至没有意识到自己犯了个错误,所以接口的设计者必须在语法层面做出限制。一个比较常见的限制是加上const
,比如在operate*
的返回类型上加上const
修饰,可以防止无意错误的赋值if (a * b = c)
。
- 接口应表现出与内置类型的一致性
让自己的类型和内置类型的一致性,比如自定义容器的接口在命名上和STL应具备一致性,可以有效防止调用者犯错误。或者你有两个对象相乘的需求,那么你最好重载operator*
而并非设计名为”multiply”的成员函数。
- 从语法层面限制调用者必须做的事
别让接口的调用者总是记得做某些事情,接口的设计者应在假定他们总是忘记这些条条框框的前提下设计接口。比如用智能指针代替原生指针就是为调用者着想的好例子。如果一个核心方法需要在使用前后设置和恢复环境(比如获取锁和归还锁),更好的做法是将设置和恢复环境设置成纯虚函数并要求调用者继承该抽象类,强制他们去实现。在核心方法前后对设置和恢复环境的调用,则应由接口设计者操心。
当方法的调用者(我们的客户)责任越少,他们可能犯的错误也就越少。
条款19:设计class犹如设计type
本条款提醒我们设计class需要注意的细节,但并没有给每一个细节提出解决方案,只是提醒而已。每次设计class时最好在脑中过一遍以下问题:
- 对象该如何创建销毁:包括构造函数、析构函数以及new和delete操作符的重构需求。
- 对象的构造函数与赋值行为应有何区别:构造函数和赋值操作符的区别,重点在资源管理上。
- 对象被拷贝时应考虑的行为:拷贝构造函数。
- 对象的合法值是什么?最好在语法层面、至少在编译前应对用户做出监督。
- 新的类型是否应该复合某个继承体系,这就包含虚函数的覆盖问题。
- 新类型和已有类型之间的隐式转换问题,这意味着类型转换函数和非explicit函数之间的取舍。
- 新类型是否需要重载操作符。
- 什么样的接口应当暴露在外,而什么样的技术应当封装在内(public和private)
- 新类型的效率、资源获取归还、线程安全性和异常安全性如何保证。
- 这个类是否具备template的潜质,如果有的话,就应改为模板类。
条款20:宁以pass-by-reference-to-const替换pass-by-value
函数接口应该以const
引用的形式传参,而不应该是按值传参,否则可能会有以下问题:
- 按值传参涉及大量参数的复制,这些副本大多是没有必要的。
- 如果拷贝构造函数设计的是深拷贝而非浅拷贝,那么拷贝的成本将远远大于拷贝某几个指针。
- 对于多态而言,将父类设计成按值传参,如果传入的是子类对象,仅会对子类对象的父类部分进行拷贝,即部分拷贝,而所有属于子类的特性将被丢弃,造成不可预知的错误,同时虚函数也不会被调用。
- 小的类型并不意味着按值传参的成本就会小。首先,类型的大小与编译器的类型和版本有很大关系,某些类型在特定编译器上编译结果会比其他编译器大得多。小的类型也无法保证在日后代码复用和重构之后,其类型始终很小。
尽管如此,面对内置类型和STL的迭代器与函数对象,我们通常还是会选择按值传参的方式设计接口。
条款21:必须返回对象时,别妄想返回其reference
这个条款的核心观点在于,不要把返回值写成引用类型,作者在条款内部详细分析了各种可能发生的错误,无论是返回一个stack对象还是heap对象,在这里不再赘述。作者最后的结论是,如果必须按值返回,那就让他去吧,多一次拷贝也是没办法的事,最多就是指望着编译器来优化。
但是对于C++11以上的编译器,我们可以采用给类型编写“转移构造函数”以及使用std::move()
函数更加优雅地消除由于拷贝造成的时间和空间的浪费。
条款22:将成员变量声明为private
先说结论——请对class内所有成员变量声明为private
,private
意味着对变量的封装。但本条款提供的更有价值的信息在于不同的属性控制——public
, private
和protected
——代表的设计思想。
简单的来说,把所有成员变量声明为private的好处有两点。首先,所有的变量都是private了,那么所有的public和protected成员都是函数了,用户在使用的时候也就无需区分,这就是语法一致性;其次,对变量的封装意味着,可以尽量减小因类型内部改变造成的类外外代码的必要改动。
一旦所有变量都被封装了起来,外部无法直接获取,那么所有类的使用者(我们称为客户,客户也可能是未来的自己,也可能是别人)想利用私有变量实现自己的业务功能时,就必须通过我们留出的接口,这样的接口便充当了一层缓冲,将类型内部的升级和改动尽可能的对客户不可见——不可见就是不会产生影响,不会产生影响就不会要求客户更改类外的代码。因此,一个设计良好的类在内部产生改动后,对整个项目的影响只应是需要重新编辑而无需改动类外部的代码。
我们接着说明,public
和protected
属性在一定程度上是等价的。一个自定义类型被设计出来就是供客户使用的,那么客户的使用方法无非是两种——用这个类创建对象或者继承这个类以设计新的类——以下简称为第一类客户和第二类客户。那么从封装的角度来说,一个public
的成员说明了类的作者决定对类的第一种客户不封装此成员,而一个protected
的成员说明了类的作者对类的第二种客户不封装此成员。也就是说,当我们把类的两种客户一视同仁了以后,public
、protected
和private
三者反应的即类设计者对类成员封装特性的不同思路——对成员封装还是不封装,如果不封装是对第一类客户不封装还是对第二类客户不封装。
条款23:宁以non-member, non-friend替换member函数
我宁愿多花一些口舌在这个条款上,一方面因为它真的很重要,另一方面是因为作者并没有把这个条款说的很清楚。
在一个类里,我愿把需要直接访问private成员的public和protected成员函数称为功能颗粒度较低的函数,原因很简单,他们涉及到对private成员的直接访问,说明他们处于封装表面的第一道防线。由若干其他public(或protected)函数集成而来的public成员函数,我愿称之为颗粒度高的函数,因为他们集成了若干颗粒度较低的任务,这就是本条款所针对的对象——那些无需直接访问private成员,而只是若干public函数集成而来的member函数。本条款告诉我们:这些函数应该尽可能放到类外。
class WebBrowser { // 一个浏览器类
public:ß
void clearCache(); // 清理缓存,直接接触私有成员
void clearHistory(); // 清理历史记录,直接接触私有成员
void clearCookies(); // 清理cookies,直接接触私有成员
void clear(); // 颗粒度较高的函数,在内部调用上边三个函数,不直接接触私有成员,本条款告诉我们这样的函数应该移至类外
}
如果高颗粒度函数设置为类内的成员函数,那么一方面他会破坏类的封装性,另一方面降低了函数的包裹弹性。
- 类的封装性
封装的作用是尽可能减小被封装成员的改变对类外代码的影响——我们希望类内的改变只影响有限的客户。一个量化某成员封装性好坏的简单方法是:看类内有多少(public或protected)函数直接访问到了这个成员,这样的函数越多,该成员的封装性就越差——该成员的改动对类外代码的影响就可能越大。回到我们的问题,高颗粒度函数在设计之时,设计者的本意就是它不应直接访问任何私有成员,而只是公有成员的简单集成,这样会最大程度维护封装性,但很可惜,这样的愿望并没有在代码层面体现出来。这个类未来的维护者(有可能是未来的你或别人)很可能忘记了这样的原始设定,而在此本应成为“高颗粒度”函数上大肆添加对私有成员的直接访问,这也就是为什么封装性可能会被间接损坏了。但设计为非成员函数就从语法上避免了这种可能性。
- 函数的包裹弹性与设计方法
将高颗粒度函数提取至类外部可以允许我们从更多维度组织代码结构,并优化编译依赖关系。我们用上边的例子说明什么是“更多维度”。clear()
函数是代码的设计者最初从浏览器的角度对低颗粒度函数做出的集成,但是如果从“cache”、“history”、和“cookies”的角度,我们又能够做出其他的集成。比如将“搜索历史记录”和“清理历史记录”集成为“定向清理历史记录”函数,将“导出缓存”和“清理缓存”集成为“导出并清理缓存”函数,这时,我们在浏览器类外做这样的集成会有更大的自由度。通常利用一些工具类如class CacheUtils
、class HistoryUtils
中的static函数来实现;又或者采用不同namespace来明确责任,将不同的高颗粒度函数和浏览器类纳入不同namespace和头文件,当我们使用不同功能时就可以include不同的头文件,而不用在面对cache的需求时不可避免的将cookies的工具函数包含进来,降低编译依存性。这也是namespace
可以跨文件带来的好处。
// 头文件 webbrowser.h 针对class WebBrowserStuff自身
namespace WebBrowserStuff {
class WebBrowser { ... }; //核心机能
}
// 头文件 webbrowsercookies.h 针对WebBrowser和cookies相关的功能
namespace WebBrowserStuff {
... //与cookies相关的工具函数
}
// 头文件 webbrowsercache.h 针对WebBrowser和cache相关的功能、
namespace WebBrowserStuff {
... //与cache相关的工具函数
}
最后要说的是,本条款讨论的是那些不直接接触私有成员的函数,如果你的public(或protected)函数必须直接访问私有成员,那请忘掉这个条款,因为把那个函数移到类外所需做的工作就比上述情况远大得多了。
条款24:若所有参数皆需类型转换,请为此采用non-member函数
这个条款告诉了我们操作符重载被重载为成员函数和非成员函数的区别。作者想给我们提个醒,如果我们在使用操作符时希望操作符的任意操作数都可能发生隐式类型转换,那么应该把该操作符重载成非成员函数。
我们首先说明:如果一个操作符是成员函数,那么它的第一个操作数(即调用对象)不会发生隐式类型转换。
首先简单讲解一下当操作符被重载成员函数时,第一个操作数特殊的身份。操作符一旦被设计为成员函数,它在被使用时的特殊性就显现出来了——单从表达式你无法直接看出是类的哪个对象在调用这个操作符函数,不是吗?例如下方的有理数类重载的操作符”+”,当我们在调用Rational z = x + y;
时,调用操作符函数的对象并没有直接显示在代码中——这个操作符的this
指针指向x
还是y
呢?
class Rational {
public:
//...
Rational operator+(const Rational rhs) const;
pricate:
//...
}
作为成员函数的操作符的第一个隐形参数”this
指针”总是指向第一个操作数,所以上边的调用也可以写成Rational z = x.operator+(y);
,这就是操作符的更像函数的调用方法。那么,做为成员函数的操作符默认操作符的第一个操作数应当是正确的类对象——编译器正式根据第一个操作数的类型来确定被调用的操作符到底属于哪一个类的。因而第一个操作数是不会发生隐式类型转换的,第一个操作数是什么类型,它就调用那个类型对应的操作符。
我们举例说明:当Ratinoal
类的构造函数允许int
类型隐式转换为Rational
类型时,Rational z = x + 2;
是可以通过编译的,因为操作符是被Rational
类型的x
调用,同时将2
隐式转换为Ratinoal
类型,完成加法。但是Rational z = 2 + x;
却会引发编译器报错,因为由于操作符的第一个操作数不会发生隐式类型转换,所以加号“+”实际上调用的是2
——一个int
类型的操作符,因此编译器会试图将Rational
类型的x
转为int
,这样是行不通的。
因此在你编写诸如加减乘除之类的(但不限于这些)操作符、并假定允许每一个操作数都发生隐式类型转换时,请不要把操作符函数重载为成员函数。因为当第一个操作数不是正确类型时,可能会引发调用的失败。解决方案是,请将操作符声明为类外的非成员函数,你可以选择友元让操作符内的运算更便于进行,也可以为私有成员封装更多接口来保证操作符的实现,这都取决于你的选择。
希望这一条款能解释清楚操作符在作为成员函数与非成员函数时的区别。此条款并没有明确说明该法则只适用于操作符,但是除了操作符外,我实在想不到更合理的用途了。
题外话:如果你想禁止隐式类型转换的发生,请把你每一个单参数构造函数后加上关键字explicit
。
条款25:考虑写出一个不抛出异常的swap函数
六、继承与面对对象设计
在设计一个与继承有关的类时,有很多事情需要提前考虑:
- 什么类型的继承?
- 接口是虚函数还是非虚的?
- 缺省参数如何设计?
想要得到以上问题的合理答案,需要考虑的事情就更多了:各种类型的继承到底意味着什么?虚函数的本质需求是什么?继承会影响名称查找吗?虚函数是否是必须的呢?有哪些替代选择?这些问题都在本章做出解答。
条款32:确定你的public继承保证了is-a关系
public继承的意思是:子类是一种特殊的父类,这就是所谓的“is-a”关系。但是本条款指出了其更深层次的意义:在使用public继承时,子类必须涵盖父类的所有特点,必须无条件继承父类的所有特性和接口。之所以单独指出这一点,是因为如果单纯偏信生活经验,会犯错误。
比如鸵鸟是不是鸟这个问题,如果我们考虑飞行这一特性(或接口),那么鸵鸟类在继承中就绝对不能用public继承鸟类,因为鸵鸟不会飞,我们要在编译阶段消除调用飞行接口的可能性;但如果我们关心的接口是下蛋的话,按照我们的法则,鸵鸟类就可以public继承鸟类。同样的道理,面对矩形和正方形,生活经验告诉我们正方形是特殊的矩形,但这并不意味着在代码中二者可以存在public的继承关系,矩形具有长和宽两个变量,但正方形无法拥有这两个变量——没有语法层面可以保证二者永远相等,那就不要用public继承。
所以在确定是否需要public继承的时候,我们首先要搞清楚子类是否必须拥有父类每一个特性,如果不是,则无论生活经验是什么,都不能视作”is-a”的关系。public继承关系不会使父类的特性或接口在子类中退化,只会使其扩充。
条款33:避免遮掩继承而来的名称
这个条款研究的是继承中多次重载的虚函数的名称遮盖问题,如果在你设计的类中没有涉及到对同名虚函数做多次重载,请忽略本条款。
在父类中,虚函数foo()
被重载了两次,可能是由于参数类型重载(foo(int)
),也可能是由于const
属性重载(foo() const
)。如果子类仅对父类中的foo()
进行了覆写,那么在子类中父类的另外两个实现(foo(int)
,foo() const
)也无法被调用,这就是名称遮盖问题——名称在作用域级别的遮盖是和参数类型以及是否虚函数无关的,即使子类重载了父类的一个同名,父类的所有同名函数在子类中都被遮盖,个人觉得是比较反直觉的一点。
如果想要重启父类中的函数名称,需要在子类有此需求的作用域中(可能是某成员函数中,可能是public 或private内)加上using Base::foo;
,即可把父类作用域汇总的同名函数拉到目标作用域中,需要注意的是,此时父类中的foo(int)
和foo() const
都会被置为可用。
如果只想把父类某个在子类中某一个已经不可见的同名函数复用,可使用inline forwarding function。
条款34:区分接口继承和实现继承
我们在条款32讨论了public继承的实际意义,我们在本条款将明确在public继承体系中,不同类型的接口——纯虚函数、虚函数和非虚函数——背后隐藏的设计逻辑。
首先需要明确的是,成员函数的接口总是会被继承,而public继承保证了,如果某个函数可施加在父类上,那么他一定能够被施加在子类上。不同类型的函数代表了父类对子类实现过程中不同的期望。
- 在父类中声明纯虚函数,是为了强制子类拥有一个接口,并强制子类提供一份实现。
- 在父类中声明虚函数,是为了强制子类拥有一个接口,并为其提供一份缺省实现。
- 在父类中声明非虚函数,是为了强制子类拥有一个接口以及规定好的实现,并不允许子类对其做任何更改(条款36要求我们不得覆写父类的非虚函数)。
在这其中,有可能出现问题的是普通虚函数,这是因为父类的缺省实现并不能保证对所有子类都适用,因而当子类忘记实现某个本应有定制版本的虚函数时,父类应从__代码层面提醒子类的设计者做相应的检查__,很可惜,普通虚函数无法实现这个功能。一种解决方案是,在父类中为纯虚函数提供一份实现,作为需要主动获取的缺省实现,当子类在实现纯虚函数时,检查后明确缺省实现可以复用,则只需调用该缺省实现即可,这个主动调用过程就是在代码层面提醒子类设计者去检查缺省实现的适用性。
从这里我们可以看出,将纯虚函数、虚函数区分开的并不是在父类有没有实现——纯虚函数也可以有实现,其二者本质区别在于父类对子类的要求不同,前者在于从编译层面提醒子类主动实现接口,后者则侧重于给予子类自由度对接口做个性化适配。非虚函数则没有给予子类任何自由度,而是要求子类坚定的遵循父类的意志,保证所有继承体系内能有其一份实现。
条款35:考虑virtual函数以外的其他选择
条款36:绝不重新定义继承而来的non-virtual函数
意思就是,如果你的函数有多态调用的需求,一定记得把它设为虚函数,否则在动态调用(基类指针指向子类对象)的时候是不会调用到子类重载过的函数的,很可能会出错。
反之同理,如果一个函数父类没有设置为虚函数,你千万千万不要在子类重载它,也会犯上边类似的错误。
理由就是,多态的动态调用中,只有虚函数是动态绑定,非虚函数是静态绑定的——指针(或引用)的静态类型是什么,就调用那个类型的函数,和动态类型无关。
话说回来,虚函数的意思是“接口一定被继承,但实现可以在子类更改”,而非虚函数的意思是“接口和实现都必须被继承”,这就是“虚”的实际意义。
条款37:绝不重新定义继承而来的缺省参数值
这个条款包含双重意义,在继承中:
- 不要更改父类非虚函数的缺省参数值,其实不要重载父类非虚函数的任何东西,不要做任何改变!
- 虚函数不要写缺省参数值,子类自然也不要改,虚函数要从始至终保持没有缺省参数值。
第一条在条款36解释过了,第二条的原因在于,缺省参数值是属于__静态绑定__的,而虚函数属于动态绑定。虚函数在大多数情况是供动态调用,而在动态调用中,子类做出的缺省参数改变其实并没有生效,反而会引起误会,让调用者误以为生效了。
缺省参数值属于静态绑定的原因是为了提高运行时效率。
如果你真的想让某一个虚函数在这个类中拥有缺省参数,那么就把这个虚函数设置成private,在public接口中重制非虚函数,让非虚函数这个“外壳”拥有缺省参数值,当然,这个外壳也是一次性的——在被继承后不要被重载。
条款38:通过复合塑膜出has-a关系,或“根据某物实现出”
两个类的关系除了继承之外,还有“一个类的对象可以作为另一个类的成员”,我们称这种关系为“类的复合”,这个条款解释什么情况下我们应该用类的复合。
第一种情况,非常简单,说明某一个类“拥有”另一个类对象作为一个属性,比如学生拥有铅笔、市民拥有身份证等,不会出错。
第二种情况被讨论的更多,即“一个类根据另一个类实现”。比如“用stack实现一个queue”,更复杂一点的情况可能是“用一个老版本的Google Chrome内核去实现一个红芯浏览器”。
这里重点需要区分第二种情形和public继承中提到的”is-a”的关系。请牢记“is-a”关系的唯一判断法则,一个类的全部属性和接口是否必须全部继承到另一个类当中?另一方面,“用一个工具类去实现另一个类”这种情况,是需要对工具类进行隐藏的,比如人们并不关心你使用stack实现的queue,所以就藏好所有stack的接口,只把queue的接口提供给人们用就好了,而红芯浏览器的开发者自然也不希望人们发现Google Chrome的内核作为底层实现工具,也需要“藏起来”的行为。
条款39:明智而审慎地使用private继承
与类的复合关系相似,private继承正是表达“通过某工具类实现另一个类”。那么相似的,工具类在目标类中自然应该被隐藏——所有接口和变量都不应对外暴露出来。这也解释了private继承的内涵,它本质是一种__技术封装__,和public继承不同的是,private继承表达的是“只有实现部分被继承,而接口部分应略去”的思想。
与private继承的内涵相对应,在private继承下,父类的所有成员都转为子类私有变量——不提供对外访问的权限,外界也无需关心子类内有关父类的任何细节。
当我们拥有“用一个类去实现另一个类”的需求的时候,如何在类的复合与private继承中做选择呢?
- 尽可能用复合,除非必要,不要采用private继承。
- 当我们需要对工具类的某些方法(虚函数)做重载时,我们应选择private继承,这些方法一般都是工具类内专门为继承而设计的调用或回调接口,需要用户自行定制实现。
如果使用private继承,我们无法防止当前子类覆写后的虚函数被它的子类继续覆写,这种要求类似于对__某个接口(函数)加上关键字final一样__。为了实现对目标类的方法的防覆写保护,我们的做法是,在目标类中声明一私有嵌套类,该嵌套类public继承工具类,并在嵌套类的实现中覆写工具类的方法。
class TargetClass { //目标类
private:
class ToolHelperClass : public ToolClass { //嵌套类,public继承工具类
public:
void someMethod() override; //本应被目标类覆写的方法在嵌套类中实现,这样TargetClass的子类就无法覆写该方法。
}
}
如此一来,目标类的子类就无法再次覆写我们想要保护的核心方法。
条款40:明智而审慎地使用多继承
原则上不提倡使用多继承,因为多继承可能会引起多父类共用父类,导致在底层子类中出现多余一份的共同祖先类的拷贝。为了避免这个问题C++引入了虚继承,但是虚继承会使子类对象变大,同时使成员数据访问速度变慢,这些都是虚继承应该付出的代价。
在不得不使用多继承时,请慎重地设计类别,尽量不要出现菱形多重继承结构(“B、C类继承自A类,D类又继承自B、C类”),即尽可能地避免虚继承,一个完好的多继承结构不应在事后被修改。虚基类中应尽可能避免存放数据。