(by Scott Meyers)
Chapter 1: Accustoming Yourself to C++
Item 1: View C++ as a federation of languages
- There is C, object-oriented C++, templates and template metaprogramming, and the STL.
Item 2: Prefer const
, enum
, and inline
to #define
- Unlike when using a
#define
, if an error occurs when using a constant, then its name is included in the error message.
- For a constant pointer in the header file, declare both the pointer and the data it points to as
const
.
- In-class initialization is allowed only for integral types, and only for constants.
- When using a macro, remember to parenthesize all the arguments, and beware expressions being evaluated multiple times if used as arguments.
Item 3: Use const
whenever possible
- Using
const
is wonderful because it allows the compiler to enforce a semantic constraint.
- Declaring an
iterator
const
means it isn't allowed to point to something different, but whatever it points to may be modified.
- One of the hallmarks of good user-defined types is that they avoid gratuitous incompatibilities with built-in types.
- A
const
member function can overload a non-const
member function, and the former will be used on const
objects.
- Bitwise
const
is C++'s definition of const
. Logical const
is when bits in the object are changed, but in ways that the client cannot detect.
- The
mutable
keyword frees non-static data members from the constraints of bitwise const
.
- By calling
static_cast
to add const
to this
, and then const_cast
to remove const
from the return value, the overloaded non-const
function can call the const
one.
Item 4: Make sure that objects are initialized before they're used
- Reading uninitialized values yields undefined behavior, so always initialize objects before you use them.
- Always listing every data member on the initialization list avoids having to remember which data members may go uninitialized.
- Within a class, data members are initialized in the order they're declared, and not in their order on the initialization list.
- If a non-local static object in one translation unit uses a non-local static object in another translation unit, it may be uninitialized, because their initialization order is undefined.
- Using
static
objects defined in functions eliminates this problem, and if you never call such a function, you don't even construct its static
object.
Chapter 2: Constructors, Destructors, and Assignment Operators
Item 5: Know what functions C++ silently writes and calls
- A compiler declares a copy constructor, copy assignment operator, and destructor if you don't declare them yourself, as well as a default constructor if you declare none.
- The generated destructor is not virtual unless the class inherits from a base class with a virtual destructor.
- You must define the copy constructor yourself if the class contains a reference member or a
const
member.
Item 6: Explicitly disallow the use of compiler-generated functions you do not want
- Declare the copy constructor and the copy assignment operator private to prevent the compiler from generating its own version.
- To stop member and friend functions from still calling them, don't actually define them; this generates an error during the linking stage.
Item 7: Declare destructors virtual in polymorphic base classes
- When a derived class object is deleted through a pointer to the base class with a non-virtual destructor, results are undefined, but typically the derived part isn't destroyed.
- If the class isn't intended to be a base class, making the destructor virtual increases its size, as this adds a
vptr
(virtual table pointer) and vtbl
(virtual table).
- The
string
class and STL container types (vector
, list
, set
, etc.) lack virtual destructors, and so should never be inherited from.
Item 8: Prevent exceptions from leaving destructors
- Depending on the circumstances, if two destructors simultaneously emit exceptions, program execution either terminates or yields undefined behavior.
- One option is to terminate the program in the destructor, thereby preventing any undefined behavior.
- The second option is to swallow the exception, but only if the program can reliably continue after the exception was ignored.
- Good practice is to try and move the operation that can generate an exception to outside the destructor.
Item 9: Never call virtual functions during construction or destruction
- During base class construction, virtual function calls never go down into a derived class, because an object is not of a derived class until its constructor begins execution.
- The same reasoning applies during destruction.
- You must ensure that your constructors or destructors don't call virtual functions, and that all the functions they call obey the same constraint.
Item 10: Have assignment operators return a reference to *this
- This is what all built-in types do, thereby allowing chained assignments, and it applies to all assignment operators (such as
operator+=
).
Item 11: Handle assignment to self in operator=
- Code that operates on references or pointers to multiple objects of the same type must consider that the objects might be the same.
- Guard against self-assignment by checking the argument's address against
this
at the top of operator+=
.
- In many cases, a careful ordering of statements can yield code that guards against both exceptions and self-assignment.
- Another alternative that guards against both exceptions and self-assignment is the "copy and swap" technique, which is covered in Item 29.
Item 12: Copy all parts of an object
- When you add new data members to a class, be sure to update its copy constructor and copy assignment operator accordingly.
- A copying function should copy all local data members, and also invoke the appropriate copying function in all base classes.
Chapter 3: Resource Management
Item 13: Use objects to manage resources
- A thrown exception or a premature
return
, continue
, or goto
statement might preclude execution from eaching a delete
statement.
- By putting resources inside objects like
auto_ptr
, we can rely on C++'s destructor invocation to ensure that the resources are released.
- Resource Acquisition Is Initialization, or RAII, means acquiring a resource and initializing its managing object happen in the same statement.
- Copying an
auto_ptr
sets it to null. While this enforces an object being managed by only one auto_ptr
, you cannot use them in STL containers.
- There is no
auto_ptr
or shared_ptr
for dynamically allocated arrays because you should be using vector
instead.
Item 14: Think carefully about copying behavior in resource-managing classes
- For resources that are not heap-based, smart pointers like
auto_ptr
and shared_ptr
are inappropriate as resource handlers.
- Policies for copying an RAII object include prohibiting copying, and reference counting, copying, and transferring ownership of the underlying resource.
Item 15: Provide access to raw resources in resource-managing classes
- An implicit conversion function on the RAII class can make access to the raw resource easier, but this increases the chance of errors.
- Often an explicit conversion function simply named
get
is preferable, even if clients must explicitly call it each time.
- Returning the raw resource violates encapsulation, but RAII classes don't exist simply to encapsulate it, but to ensure that it is released.
Item 16: Use the same form in corresponding uses of new
and delete
- Using the expression
delete
when delete []
should be used results in undefined behavior.
- The memory for an array usually includes the size of the array, so that the
delete
operator knows how many destructors to call.
- Use the same form of
new
in all constructors that initialize a pointer member, or else you may use the wrong form of delete
.
- The
string
and vector
classes largely eliminate the need to dynamically allocate an array.
Item 17: Store new
objects in smart pointers in standalone statements
- Compilers are given less leeway in reordering operations across statements than within them.
Chapter 4: Designs and Declarations
Item 18: Make interfaces easy to use correctly and hard to use incorrectly
- The type system is your primary ally in preventing undesirable code from compiling.
- Restrict what can be done with a type. A common way to impose restrictions is to add
const
wherever you can.
- Avoid gratuitous incompatibilities with the built-in types so that interfaces behave consistently, thereby reducing mental friction.
- Any interface that requires clients to remember to do something is prone to incorrect use, because clients can forget to do it.
- In many applications, the additional runtime costs of resource managers are unnoticeable, but the reduction in client errors will be noticed by everyone.
Item 19: Treat class design as type design
- Approach class design with the same care that language designers lavish on the design of built-in types.
- Good types have a natural syntax, intuitive semantics, and one or more efficient implementations.
- If you inherit from existing classes, you are constrained by their design, particularly by whether their functions are virtual or not.
- Guarantees made with respect to performance, exception safety, and resource usage impose constraints on your implementation.
- If you're defining a whole family of types, you don't want to define a new class, you want to define a new class template.
- If you're only subclassing so you can add functionality to an existing class, consider non-member functions or templates instead.
Item 20: Prefer pass-by-reference-to-const
to pass-by-value
- Passing by reference eliminates the slicing problem, where passing a derived class object to a function that accepts a base class object calls the base class copy constructor.
- References are typically implemented as pointers, so passing built-in types like
int
by value is usually more efficient.
- Implementers of iterators and function objects ensure that they are efficient to copy and are not subject to the slicing problem.
- Avoid passing a user-defined type by value because while its size may be small now, that is subject to change with its implementation.
Item 21: Don't try to return a reference when you must return an object
- A function should never return a reference or pointer to a local object that is destroyed when the function exits.
- Returning a reference from a function like
operator*
is incorrect. Such a function must return a new object.
- In some cases the construction and destruction of such a return value can be safely eliminated by the compiler.
Item 22: Declare data members private
- Many data members should be hidden, and rarely does every data member require a getter and a setter.
- Only functional interfaces makes it easy to notify other objects when variables are read or written, to verify class invariants and function pre- and postconditions, to perform synchronization in threaded environments, etc.
- Public means unencapsulated, which means an unchangeable implementation, especially for classes that are widely used.
- Protected data is as unencapsulated as public data, since changing such a data member could break all derived classes that use it, which is an unknowably large amount of code.
Item 23: Prefer non-member non-friend functions to member functions
- The more functions that can access data, the less the data is encapsulated, and the harder it is to change the characteristics of the data.
- Unlike a member function, a non-member non-friend function doesn't increase the count of functions that can access the private parts of a class.
- Consider putting the non-member function in the same namespace as the class it operates on.
- Partitioning functionality in a namespace across multiple files promotes clean organization, and clients can freely add more functions to the namespace.
Item 24: Declare non-member functions when type conversions should apply to all parameters
- For overloaded operators, compilers will also look at non-member functions accepting two parameters in the namespace or global scope.
- Parameters are eligible for implicit type conversion only if they are listed on the parameter list. The object on which the member function is invoked is never eligible.
- To support mixed mode arithmetic with operator overloading, make operators non-member functions accepting both operands as arguments.
- The opposite of a member function is a non-member function, not a friend function. Avoid friend functions when you can.
Item 25: Consider support for a non-throwing swap
- If using the "pimp idiom," define a member function named
swap
that does the actual swapping, then specialize std::swap
to call that member function.
- It's okay to totally specialize templates in
std
, adding new templates, classes, functions, or anything else to std
results in undefined behavior.
- In addition to the specialization of
std::swap
, write a non-member version of swap
in the same namespace of your class.
- By prefacing a call to
swap
with using std::swap
, compilers will look for the namespace definition first, then the specialization in std
, and finally the general form.
Chapter 5: Implementations
Item 26: Postpone variable definitions as long as possible
- Postponing declaring a variable until you have its initialization arguments avoids unnecessary default constructions.
- Assigning a variable defined outside a loop is more efficient than initializing it on ever iteration, because it avoids destructing when each iteration completes.
Item 27: Minimize casting
- Casts can subvert the type system, which is there to ensure that you're not trying to perform any unsafe or nonsensical operations on objects.
- Only use
reinterpret_cast
for low-level casts in low-level code, such as from a pointer to an int
. It yields unportable results.
- Use
static_cast
to force implicit conversions as well as the reverse of such conversions, with the exception of const
to non-const
.
- Prefer the explicit, new-style casts. They are easier to search for, and their narrow purpose makes it possible for compilers to diagnose usage errors.
- Type conversions of any kind, either explicit via casts or implicit by compilers, often lead to additional code that is executed at runtime.
- Avoid making assumptions about how things are laid out in C++. Making casts based on such assumptions leads to undefined behavior.
- A
dynamic_cast
can have a significant runtime cost. If you need to perform derived class operations through a pointer or reference to the base class, explore alternative designs.
- Casts should be isolated as much as possible, typically hidden inside functions whose interfaces shield callers from the work done inside.
Item 28: Avoid returning "handles" to object internals
- A data member is only as encapsulated as the most accessible function returning a reference to it.
- Returning
const
references to data members can still lead to dangling handles, which refer to parts of objects that don't exist any longer.
- Functions that return a handle to an object internal are the exception and not the rule.
Item 29: Strive for exception-safe code
- Exception-safe functions don't leak resources, and don't allow data structures or objects to enter a corrupted state.
- The basic guarantee ensures that the program remains in a valid state if an exception is thrown, but that exact state may ot be predictable. The strong guarantee ensures that the state is unchanged.
- Often you can't guarantee that no exceptions are thrown, because anything that dynamically allocates memory can throw a
bad_alloc
exception.
- When functions have side-effects on non-local data instead of operating exclusively on local state, it's much harder to offer the strong guarantee.
- If a system has even a single function that's not exception-safe, then the system as a whole is not exception-safe.
- A function's exception-safety guarantee is a visible part of its interface, and you should choose it as deliberately as you choose all other interface aspects.
Item 30: Understand the ins and outs of inlining
- Compiler optimizations are typically designed for stretches of code that lack function calls, so inlining allows more optimizations.
- Inlined code can also lead to additional paging, a reduced instruction cache hit rate, and their accompanying performance penalties.
- The
inline
keyword is a request that compilers may ignore, and only the most trivial functions that are not virtual may be inlined.
- Even empty constructors and destructors are unlikely to be inlined, as they implicitly call the constructors of base classes and data members.
- Debuggers have trouble with inlined functions. For example, you can't set a breakpoint in a function that isn't there.
Item 31: Minimize compilation dependencies between files
- Instead of trying to forward-declare parts of the standard library, use the proper
#include
statements and be done with it.
- You never need a class definition to declare a function using that class. Forward declare the class, and shift the burden of including its definition to clients that call the function.
- A class that employs the pimpl idiom is often called a handle class.
- If a function is declared as pure virtual in an interface class, then there is no need to include the keyword
virtual
in its declaration in the subclass.
- Handle classes and interface classes decouple interfaces from implementations and thereby reduce compilation dependencies between files.
Chapter 6: Inheritance and Object-Oriented Design
Item 32: Make sure public inheritance models "is-a"
- The most important rule in object-oriented programming in C++ is that public inheritance means "is-a".
- There is no one ideal design for all software. The best design depends on what the system is expected to do, both now and in the future.
- Good interfaces prevent invalid code from compiling. Prefer design that rejects operations during compilation than one that rejects at runtime.
- A class named
Square
extending Rectangle
is a classic example of the fragile nature of class hierarchies.
Item 33: Avoid hiding inherited names
- If a function in a derived class has the same name as a function in the base class, it hides all overloaded forms of that function in the base class.
- To preserve the is-a relationship of inheritance, the derived class must include a
using
declaration to inherit all overloaded forms of a function with a given name.
Item 34: Differentiate between inheritance of interface and inheritance of implementation
- Pure virtual functions must be redeclared by any concrete class that inherits them.
- A simple virtual function allows a derived class to inherit a function interface as well as an implementation.
- A pure virtual function can still have an implementations of its own. A subclass must redeclare it, but it can call down to this "default" implementation.
- A non-virtual function serves a mandatory implementation, and should never be redefined in a derived class.
- Don't blindly declare all member functions virtual, and don't blindly declare all member functions non-virtual. Consider each one individually.
Item 35: Consider alternatives to virtual
functions
- When a non-virtual member function calls a private virtual member function, subclasses can redefine the latter. This is a form of the template method design pattern.
- The only way to resolve the need for non-member functions to access the non-public parts of a class is to weaken its encapsulation.
- The
tr1::function
class can refer to anything that acts like a function and returns a type convertible to the specified type.
- The "standard" strategy pattern offers the possibility that an existing strategy can be tweaked via defining a subclass.
Item 36: Never redefine an inherited non-virtual function
- Non-virtual functions are statically bound, so calling one on a base class pointer or reference uses the base class implementation, and not any derived class implementation.
- A non-virtual function is an invariant over specialization for the base class, and so no derived class should try to redefine it.
- Item 7, which warns against not specifying a virtual destructor in a base class, is a special case of this item.
Item 37: Never redefine a function's inherited default parameter value
- An object's dynamic type is determined by the object to which it currently refers, which in turn determines how it will behave.
- Default parameters are statically bound, so invoking a virtual function defined in a derived class uses a default parameter value from the base class.
- To avoid duplication of the default parameter, use the non-virtual interface idiom, where the default parameter is only defined once in the public non-virtual function.
Item 38: Model "has-a" or "is-implemented-in-terms-of" through composition
- Composition in the application domain expresses a has-a relationship, while in the implementation domain it expresses an is-implemented-in-terms-of relationship.
- Inappropriate subclassing violates the is-a principle, and should be replaced with composition.
Item 39: Use private inheritance judiciously
- With private inheritance, you cannot assign a derived object to a base class pointer, and protected and public members in the base class are private in the derived class.
- Use composition whenever you can, and use private inheritance whenever you must.
- Use private inheritance if two classes don't have an is-a relationship, but one needs to access the protected members or redefine a virtual function in the other.
- If a class privately inherits from another and defines a virtual function, its own subclasses can redefine that function, even if they can't call it.
Item 40: Use multiple inheritance judiciously
- If a function is defined in two base classes, then a call in the derived class to that function is always ambiguous, even if only one definition is accessible.
- Virtual inheritance prevents replicating data in the base class when multiple inheritance is used, but it's slower and creates larger objects.
- The one reasonable of multiple inheritance is to combine public inheritance of an interface with private inheritance of an implementation.
- If the only design you can come up with involves multiple inheritance, you should think a little harder.
Tags:
reading
language
native
Last modified 07 October 2024