Readings: Effective C++
Title: Effective C++ Third Edition. 55 Specific Ways to Improve Your Program and Designs
Author: Scott Meyers
General Review
Personal notes for effective C++. Although this book is based on the legacy C++ versions, it still cover key principles of the language. It also highlights corner cases that could be detrimental to the project.
Chapter 1
Item 1: View C++ as a federation of languages
C++ can be broken down into 4 sub languages:
- C: Working with C part of C++
- Object-Oriented C++: Working with classes and OOP principles in C++
- Template C++: Working with generic programming in C++
- STL: Working with C++ STL library.
Rules for effective C++ will on the sub language.
Item 2: Prefer consts
, enums
, and inlines
to #define
This item mainly discuss effective methods to declare constants and alternative to macro functions.
When we use #define
, the compiler will replace the reference to the macro
with the defined value.
Declaring constants with #define
Drawbacks:
- The macro will not enter the symbol table which will result in confusing error messages. Where you do not know where is the source of the error.
- Less efficient as the preprocessor will create a copy of the value each time the macro is referenced
Alternative:
-
Bad:
#define ASPECT_RATIO 1.653
-
Good:
const double AspectRatio = 1.653;
Declaring constants for class
When declaring constant that should only exist within a class scope, use
static const
.
class GamePlayer {
private:
static const int NumTurns = 5;
}
Rationale:
- Using
static
will ensure that there is at most one copy of the constant
Inline function
Use inline functions instead of #define
macros.
Item 3: Use const
whenever possible
const
pointers
-
const char * p
: Whenconst
is on the left of a pointer, it means that the data isconst
. You cannot change the value that the pointer is pointing to. However, you are allowed to change pointer.int i = 1; int j = 2; const int * ptr = &i; *ptr = 3; // compilation error ptr = &i; // ok
-
char * const p
: Whenconst
is on the right of a pointer, it means that the pointer isconst
and you cannot change the variable to a new pointer. However, you are allowed to change the value the pointer points to.int i = 1; int j = 2; int * const ptr = &i; *ptr = 3; // ok ptr = &j; // compilation error
const
STL iterator
- Mimicks the behaviour of a pointer
const std:xxx<T>iterator iter
same asT* const
: Can change the value the iterator points to but cannot change the iterator to a new iterator.std:xxx<T>const_iterator iter
same asconst T*
: Cannot change the value the iterator points to but can change the iterator to a new iterator.
Function return const
const Rational oeprator* (const Rational& lhs, const& rhs)
Generally it is not appropriate to return const
type as the value
can be easily copied when assigning to a new variable but it prevent
the following human error:
(a*b) = c
const
Member Functions
Should use const
member function to state which function
would modify an object and which would not.
Functions can also be overloaded using const
.
class TextBlock {
public:
const char& operator[](const std::size_t pos) const; // for const objects
char& operator[](const std::size_t pos); // for non-const objects
}
Avoid duplicate code in const
and non-const
member functions by casting
non-const
*this
to const *this
. This is ok as we are adding tightening
the non-const
by adding const
constraint.
const char& operator[](const std::size_t pos) const{...};
char& operator[](const std::size_t pos) {
return
const_cast<char&>(
static_cast<cosnt TextBlock&>(*this)[position]
);
}
Use mutable
to allow data member to be modified in const
member functions.
Usually useful when implementing caching functions.
mutable bool lengthIsValid;
std::size_t length() const {
if(!lengthIsValid) {
lengthIsValid = true; // modifying data member in const function
}
return textLength;
}
Item 4: Make sure that objects are initialized before they’re used
Initializing member variables
Order of class initialization:
- Base class initialized before derived class
- Data member are initialized in the order in which they are declared.
Bad example:
class ABEntry {
private:
std::string theName;
std::string theAddress;
std::list<Phonenumber> thePhones;
int numTimesCosulted;
public:
ABEntry(const std::string& name, const std::string& address,
const std::list<Phonenumber>& phones) {
theName = name; // these are all assignments
theAddress = address;
thePhones = phones;
numTimesCosulted = ;
}
}
- Data members are initialized before the body of constructor.
- Inefficient as the default constructors are called on the non-integral types and then copy assignment constructor called in constructor.
Good example (using initialization list):
class ABEntry {
private:
std::string theName;
std::string theAddress;
std::list<Phonenumber> thePhones;
int numTimesCosulted;
public:
ABEntry(const std::string& name, const std::string& address,
const std::list<Phonenumber>& phones) :
theName(name),
theAddress(address),
thePhones(phones),
numTimesCosulted(0)
{}
}
- More efficient with a single copy constructor than a default constructor followed by copy assignment constructor.
- Might be required when initializing for
const
or reference data members.
Non-local static objects in different translation unit
- static object are objects that exists when it constructed to the end of program. Does not exist on the stack or heap. They are destoryed when
- non-local static object are static objects outside of a function.
- translation unit: source code that give rise to single object file.
Problem of depending on a non-local static object in different translation unit:
// some.cpp
class FileSystem {
public:
...
std::size_t numDisk() const;
...
}
extern FileSystem tfs;
// other.cpp
class Directory {
public:
Directory(params) {
std::size_t disks = tfs.numDisks();
}
}
Directory tmpDir(params);
- Relative order initialization of non-local static object in different
translation unit is undefined.
tmpDir
could be initialized beforetfs
.
Solution (reference-returning functions):
// some.cpp
class FileSystem {
public:
...
FileSystem& tfs() {
static FileSystem fs;
return fs;
}
std::size_t numDisk() const;
...
}
extern FileSystem tfs;
// other.cpp
class Directory {
public:
Directory(params) {
std::size_t disks = tfs().numDisks();
}
}
Directory tmpDir(params);
- This will guarantee that the static object is always initialized.
- If you do not call the method you will never incur the cost of constructing.
- Caveats:
- Does not work well with multi-threading. Need to call the reference-returning functions in master thread before forking child thread so that all child thread will have the same reference.
Chapter 2
Item 5: Know what functions C++ silently writes and calls
class Empty {
Empty(){} // default constructor
Empty(const Empty& rhs){} // copy constructor
Empty& operator=(const Empty& rhs){} // copy assignment constructor
~Empty() {} // destructor
}
By default, the compile will generate the following constructors inline:
- Generated default constructor and destructor:
- Default constructor:
Emtpy Empty e1; // defa
- Create a default constructor that calls the base class’ constructor/destructor
- Call the default constructor/destructor of data members
- Exception: When any constructor (default/non-default) is declared, the compiler will not create a default constructor but will still create copy constructor and copy assignment constructor.
- Default constructor:
- Generated copy constructor:
- Copy constructor:
NamedObject<int> no1("Smallest Prime Number", 2); NamedObject<int> no2(no1); // copy constructor invoked
- The generated copy constructor will call the copy constructor on each data member and base class.
- Copy constructor:
- Generated copy assignment:
- Copy assignment constructor:
Empty e1; Empty e2; e2 = e1; // copy assignment constructor
- Call the copy assignment constructor for each data member and base class.
- Caveats: If the data member is a reference to an object it will not work as c++ does not
allow changing reference of a reference variable. You will need to define your own copy assignment
constructor if you have a reference variable.
int a = 2; int b = 4; int& ref = a; // cannot change ref to point to b instead
- Copy assignment constructor:
Item 6: Explicitly disallow the use of compiler-generated functions you do not want.
In some cases you would want to disable the compiler from generating copy assignment constructor and copy constructor (ie: Wanting an object to be unique).
Method 1: Declare copy assignment constructor and copy constructor as private and do not define them.
class HomeForSale {
private:
HomeForSale(const HomeForSale&);
HomeForSale& HomeForSale=(const HomeForSale&);
}
- Making private: Prevent calling of constructor outside of class
- Not defining constructor: Throw link time error when member functions try to call constructor.
- This method is widely use
Method 2: Set assignment constructor and copy constructor private in base class.
class Uncopyable {
private:
Uncopyable(const Uncopyable&);
Uncopyabl& operator=(const Uncopyable&);
}
class HomeForSale : private Uncopyable {}
This result in compilation error when trying to use copy assignment/ copy constructor. It will call the base counter part which are private.
Item 7: Declare destructor virtual in polymorphic base classes.
Example:
class TimeKeeper {
public:
TimeKeeper();
~TimeKeeper();
}
class AtomicClock : public TimeKeeper {...}
Problems with non-virtual destructor:
- When a pointer to a derived class is cast to a base class, only the base class destructor would be called instead of the derived class.
- This results in the data members of the base class being destructed but the data members of derived class being untouched.
- Result in partially destructed objects and memory leaks
Solution: Use virtual destructor for all base classes.
Virtual function under the hood:
- Object requires to store information at runtime to determine which virtual method to be called. ie:
class B{ virtual foo(); } class D1 : B { virtual foo(); } class D2 : B { virtual foo(); } int i; cin >> i; B* b = i > 5 ? new D1() : new D2(); // We will only know at run time which virtual method to call. b->foo()
- Each object has a virtual table pointer(
vptr
) that points to an array of pointer virtual table (vtbl
) - Each
vtbl
will point to a function that should be invoked. - Drawbacks Storing virtual table and pointer will increase space
Creating an abstract class:
- Use pure virtual destructor
- Need to add definition to the destructor of the abstract class as the base class destructor(abstract class) would be called after the derived class destructor.
clas AWOV {
public:
virtual ~AWOV() = 0;
}
AWOV::~AWOV() {} // define virtual destructor
Item 8: Prevent exceptions from leaving destructor
Throwing exceptions in destructor could result in multiple active exceptions (destructing vector of objs) which would lead to undefined behaviour.
Instead, abstract out the logic to a separate public function to allow clients to call and handle it. If the client did not call it, execute the function in the destructor but catch all exceptions to prevent UB.
Item 9: Never call virtual functions during construction or destruction
Understanding base class construction. When a derived class is constructed.
- It will first call the base class constructor
- If the base class constructor is not explicitly constructed, the base class default constructor will be called
- Call base class constructor by:
class D : B { D() : B() {} D(&D) : B(D) {...} }
- Only base class data members will be initialized. Derived class data members will not be initialized yet.
- Virtual functions in base class will not point to derived class’ function during base class constructor
- Why: derived class data members will not initialized. Calling the derived class function before data member initialization will lead to UB.
Calling pure virtual function in base class constructor/destructor
- Results in link error: base class calls the pure virtual function that is not defined
- Could bypass link error by calling the function through another function
class Transaction {
public:
Transaction()
{init)(;)}
virtual void logTransaction() const = 0;
private:
void init()
{
...
logTransaction() // call to virtual function
}
}
Solution: Pass polymorphic data through method args instead of virtual function
class Transaction {
public:
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const;
}
class BuyTransaction : public Transaction {
public:
BuyTransaction(parameters)
: Transaction(createLogString(parameters)) // should not access data members before derived class constructor
{
}
}
Item 10: Have assignment operator return a reference to *this
By convention, we should return a reference to the current object for all assignment operators
Widget& operator(const Widget& rhs){
...
return *this
}
This will allow you to chain assignment operators
int x,y,z;
x = y = z = 15;
- Assignment operations are right associative:
x=(y=(z=15))
Item 11: Handle assignment to self in operator=
Non-direct ways for potential self-assignment:
- Different pointer to same reference
a[i] = a[j] *px = *py
- Base class reference/pointer could point to a derived class
Base& rb; Drirved* pd; rb = *pd; // could be the same
Without handling self: Deleting self will result to the rhs
to be deleted as well
Widget&
Widget::operator=(const Widget& rhs)
{
delete pb;
pb = new Bitmap(*rhs.pb) // rhs.pb already deleted
return *this
}
Should place delete self as last statement
Widget& Widget::operator=(const Widget& rhs)
{
if(this == &rhs) return *this;
delete pb;
pb = new Bitmap(*rhs.pb) // if exception thrown here, pb will point to a deleted addr
return *this;
}
Instead should:
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap* pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrig; // only delete after pb points to a valid address
return *this;
}
Copy-and-swap solution
Make a copy of the rhs
then swap it to the current file
Widget& Widget::operator=(const Widget& rhs) {
Widget temp(rhs); // make a copy
swap(temp); // swap to *this
return *this;
}
Item 12: Copy all parts of an object
- Only copy-constructor and copy assignment operator should copy an object
- All local data members should be initialized in the copy constructors
- Invoke the copying function of base class too. Failing to do will result in the base class member data to not be initialized
D(const D& rhs)
: B(rhs),
m_data(rhs.m_data)
{
}
D::operator=(const D& rhs)
{
B::operator=(rhs);
m_data = rhs.m_data
return *this
}
Bad practices:
- Do not call the copy assignment from copy constructor
- Do not call the copy constructor from copy assignment
- Use a third
init()
method instead
Chapter 3: Resource Management
The contents of this chapter is slightly outdated as it still
refers to auto_ptr
(deprecated) and not the new unique_ptr
.
Item 13: Use objects to manage resource
The main takeaway for this item is to use object to manage
heap based resource instead of raw pointers (just T*
)
Drawbacks of raw pointers
- Whenever you dynamically allocate memory, you will need to manually deallocate it.
- Your instruction to deallocate memory might not happen when there is an early return, exception, break in a loop.
- This will result in a memory leak
Resource Acquisition Is Initialization (RAII)
Using an object that acquires a resource (dynamically allocated resource) instead of managing raw pointers.
Key Ideas:
- All object’s (on stack) destructor will be called once it goes out of scope
- Tying a resource to an object will allow the destructor of the object to clean up the resource
- std provides smart pointers that acts as a wrapper to objects
auto_ptr
(deprecated) andshared_ptr
Smart Pointers
- All resource are assigned to a managing object
- Destructor of objects are called automatically when it is destroyed
- Drawbacks:
- Only calls
delete
on the pointer instead ofdelete[]
. If the resource is a dynamically allocated array, only the first pointer will be deleted but everything else. Compiles but leads to UB
- Only calls
auto_ptr (deprecated)
- wraps around a pointer and calls the destructor of the pointer once it goes out of scope
void f() { std::auto_ptr<Foo> f(new Foo()); }
- Drawbacks:
- Does not exhibit normal copy constructor and copy assignment behaviour. The objects are “moved”
instead of being copied
std::auto_ptr<Foo> f1(new Foo()); // f1 points to the new Foo std::auto_ptr<Foo> f2(f1) // f2 points to the previous new Foo // f1 now points to null f1 = f2; // f1 points to the previous new Foo but // f2 points to null
- Will not work in STL containers as it require elements to exhibit normal copying behaviour.
- Does not exhibit normal copy constructor and copy assignment behaviour. The objects are “moved”
instead of being copied
shared_ptr
- Uses reference-counting to determine if the resource can be deleted.
- Every time the object (
shared_ptr
) is copied/assigned it will increase the reference. - Call destructor on the pointer once the counter goes to 0.
- Every time the object (
- Caveats: Cannot break cycles. If two objects that are unused has a reference to each other,
the counter will never go 0.
- Unlike Java GC, there is no mark, sweep, merge steps to determine if an two objects are stuck in a cyclic reference
Item 14: Think carefully about copying behaviour in resource-managing class
You can implement your own RAII to manage heap resource. However, based on your requirements, you might want to handle copying separately.
- Prohibit Copying
- If a resource should not be copied (Lock) for a mutex, use Uncopyable
- Reference Counting
- If the resource should be destroyed once all references to has been deleted.
- Use a
shared_ptr
data member. - If we do not want to delete the underlying resource (unlocking a mutex instead of deleting),
we can provide
shared_ptr
with a deleter funtcion. This will cause theshared_ptr
to call the function instead of the destructor of the resource. - Copying the underlying resource (more than one of the resource could exists), we should perform a deep copy of the data in the heap instead of just copying the pointer.
Item 15: Provide access to raw resource in resource-managing classes
There are times you would need to access the underlying heap based resource from an RAII. You can use the following approach:
- Explicit conversion:
- Provide a
raii.get()
function to get the underlying heap based resource - Overload pointer dereferencing operator (
->
) - Drawbacks: might be intrusive to the code
- Provide a
- Implicit conversion:
- Allow casting from
raii
to the underlying heap based resource pointer. - Allow clients to easily use api that require heap based resource pointer without
having to call explicit conversion
class RAII { private: Foo f; // heap based resource public: operator Foo() const{return f;} // Overload casting operator to return // the underlying resource when casted to it } void Bar(Foo f); // Require heap based resource RAII r; Bar(r); // cast RAII to Foo
- Drawbacks: allowed accidental casting from RAII to the underlying resource.
- Allow casting from
Item 16: Use the same form in corresponding uses of new
and delete
We should always use delete[]
iff new T[]
was used and delete
iff new T
was used.
The reason is that when we call delete[]
or delete
on address we do not
validate what the address contains and call the destructor on the memory.
When we call new
or new T[]
the heap will look like (depends on compiler):
Single Object: | Object |
Array : | n | Object | Object | Object |
- Calling
delete[]
on a single object will result in the firstk
bytes being assumed to be the size of the array (but is actually part of the Object) and the destructor will be called on the next contiguousn'
location in memory. - Calling
delete
on an array will result in the address for the size of the array to be deleted and part of the first object to deleted.
Item 17: Store newed
objects in smart pointers in standalone statement
To guarantee that there will be no resource leak when using smart pointers, we will need to make sure that no exceptions are thrown between heap based resource being allocated and smart pointer being constructed.
The easy solution is to store new
object in smart pointer in a standalone statement
that guarantees no exceptions thrown
Negative example:
processWidget(shared_ptr<Widget>(new Widget), foo());
- We will need to perform the following argument evaluations:
- Execute
new Widget
- Construct
shared_ptr
- Call
foo
- Execute
- In cpp, the compiler is allowed to reorder the evaluations of arguments
- Compiler can reorder to: Execute
new Widget
-> Callfoo
-> Constructshared_ptr
- If there is an exception in
foo
, thenew
Widget would be leaked and will not be cleaned up byshared_ptr
- If there is an exception in
Chapter 4: Design and Declarations
Item 18: Make interfaces easy to use correctly and hard to use incorrectly
Good API design should prevent any client errors by making it easy to use and hard for the clients to use incorrectly. We can do so with the following methods:
- Strongly typed parameters:
- When a function takes in multiple parameters of similar type, client could easily make mistakes by using the wrong ordering of parameters
- If there is a small range of allowed argument for a parameter, use class function
as a constructor.
struct Month {static Month::Jan(){return Month(1)}}; struct Day {}; struct Year {}; Date d(Month::Mar(), Day(30), Year(1995))
- Return
const
to prevent invalid assignment - Return smart pointers
- Do not rely on client calling delete
shared_ptr
: Allow you to add your owndeleter
to perform clean up once the references go to 0
- Interface should be consistent with standard library interfaces
Item 19: Treat class design as type design
This item mainly state the various questions we should ask ourselves when designing a new type.
Item 20: Prefer pass-by-reference-to-const to pass-by-value
Pass-by-value
- Make a copy of the actual argument. Calls the copy constructor of all the data members and base class
- By default all arguments are passed by value
- Object Slicing: Derived object slice off derived class data members when copying.
- Pass a derived class to a function that takes in the base class
- Causes the only the base class copy constructor to be called on the derived object
- Applies to user defined type with small constructor
- Compiler treats user-defined type differently from built-in types
- The constructor overhead of user-defined types might change
- Exceptions:
- Built-in types: same overhead as passing a pointer
- stl iterator: act as pointers
- function objects
Pass-by-reference-to-const
- When we pass by reference we are essentially passing a pointer as an argument to the function
- Use const so that the caller will know that argument would not be mutated
Item 21: Don’t try to return a reference when you must return an object
When returning an object as reference from the local stack (function body), the object will destructed once it returns. The reference that the function returns will point to an object that is destructed.
Notes
- Returning reference of object on stack:
- Objects would be destructed at the end of the function
- Reference to destructed object will still be returned -> leads to UB
- Returning reference of to object on heap:
- Easily lead to memory leak as we do not know who should delete the object
Item 22: Declare data members private
This items argues why data members should be private and does not cover any c++ specific points.
Arguments:
- Syntactic consistency
- Encapsulation: allow you to easily change the implementation of the data member.
- ie: changing the data member from a member variable to a combination of member variables
Item 23: Prefer non-member non-friend functions to member functions
This item mainly applies to member functions that does not actually require to be a member function (utility functions)
Member function drawbacks:
- Reduces encapsulation as it exposes internal data members
- Does not allow separation of utility functions.
- If a client imports a class, it will have to import all utility functions instead of what they just need
Solution: Create multiple header files that contains different type of utility functions on the same class
Item 24: Declare non-member functions when type conversion should apply to all parameters
How type conversion works:
- Given a type
T1
and the object can only be constructed from typeT2
- type coversion will work if there is conversion from
T2 -> T1
.
- type coversion will work if there is conversion from
- A temporary
T2
object will be created fromT1
withT2(T1)
T2
will be provided as argument to the object’s constructor
How operators work:
- For operators (
*
,+
, …), the compiler will try to look for valid operator declared in the namespace scope to perform the operator on the 2 parameter - Type conversion will be used if the operator is not
explicit
For operators that should perform implicit conversion on lhs/rhs, use non-members so that both the parameters can be converted.
class Rational {
}
const Rational operator*(const Rational& lhs, const Rational& rhs)
Rational oneForth(1,4);
Rational result;
result = oneForth* 2 // convert 2 to Rational
result = 2* operator // convert 2 to Rational
Item 25: Consider support for a non-throwing swap
- Swap operations should not throw exceptions as many operations rely on swap operations to prevent memory leak
Chapter 5: Implementations
Item 26: Postpone variable definition as long as possible
This item argues that we should defer any variable definition to as long as possible. This includes:
- Defining the variable right before it is needed:
- Prevent unneccassary calls to the constructor if an exception
is thrown or returned before it is used.
... string encrypted; if (...) throw exception; // encrypted is constructed and destructed foo(encrypted);
- Prevent unneccassary calls to the constructor if an exception
is thrown or returned before it is used.
- Do not define a variable with default constructor then assign it to a new
value afterwards
- Prevent extra call to constructor then copy assignment when you could just
have a single call to copy constructor
... string encrypted; encrypted = password; foo(encrypted); //vs string encrypted(password); foo(encrypted);
- Prevent extra call to constructor then copy assignment when you could just
have a single call to copy constructor
Item 27: Minimize casting
Types of cast
- C-style:
(T) expression
- Function style:
T(expression)
. Call the type’s cast function - C++ style:
const_cast<T>(expression)
: cast away the const from an objectdynamic_cast<T>(expression)
: cast from a base class to a derived class. Determine polymorphic type in runtimereinterpret_cast<T>(expression)
: casting a pointer to an int (for low level)static_cast<T>(expression)
: for implicit conversion (ie int to double)
Problems with casting:
- Casting a pointer of a derived class to a base class could result in additional cost of
getting the base class pointer.
- In C++ the pointer for a derived class
&d
might differ in address from the pointer of a base class from that derived class pointerDerived d; Base* b = &d; assert(b!=d);
- In C++ the pointer for a derived class
- When casting a derived object to base object with
static_cast
. C++ will create a temporary copy of the base object and call the function on it.- Solution is to call base class function directly
Base::foo()
in the derived class function
- Solution is to call base class function directly
dynamic_cast
is slow: under the hood performs string matching to the class name- if you find yourself doing multiple
if(auto t = dynamic_cast<T>(ptr))
a lot should use virtual functions instead
- if you find yourself doing multiple
Item 28: Avoid returning “handles” to object internals
We should avoid returning a handler (reference, pointer, iterator) to an internal data structure in member functions.
Rationale:
- If the return type is not const, it will allow the client side to modify internal data member using the member function.
- Result in dangling handles, if a temporary object is created and only the handler is assigned to a variable. The temporary object will destroyed but the handler will be left dangling.
Item 29: Strive for exception-safe code
Goals of exception-safe function
- Leak no resource: should not leak any resource
- Don’t allow data structure to become corrupted: A function should not be in an invalid state no matter what
Types of exception-safe guarantees
- Basic guarantees
- When an exception is thrown a program will be in a valid state. Might be a the same state before exception is thrown
- Strong guarantees
- State changes are atomic. When an exception is thrown a program will revert to the previous state before the exception is thrown
- Nothrow guarantees
- function does not throw any exception
It is ok to only offer basic guarantee (depend on basic guarantee code) but should not be non exception-safe.
How to achieve exception-safe
The general way to achieve exception-safe is to use the Pimpl idiom with copy and swap
- Use pimpl to allow the underlying data member to be easily copied to a temp copy
- Perform necessary changes to the temp copy
- Swap the temp copy to replace the current copy
struct PMImpl {
std::shared_ptr<Image> bgImage;
int imageChanges;
}
class PrettyMenu {
private:
Mutex mutex;
std::shared_ptr<PMImpl> pImpl;
void changeBackGround(std::istream& imgSrc) {
using std::swap;
Lock ml(&mutex); // use RAII to prevent resouce leak
std::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); // create a temporary copy of impl
pNew->bgImage.reset(new Image(imgSrc));
++pNew->imageChanges;
swap(pImpl, pNew); // only change state if no exception
}
}
- Caveats: Pimpl + copy and swap will only work when there are no side effects to outside the local data state.
Item 30: Understand the ins and out of inlining
Overhead of a function call:
- Saving the current registers on the current function stack
- Pushing the new function argument into the new function stack
- Increment the stack pointer
- Jump to the new instruction of the new function
- When the function is done, need to restore the previous function stack
inline
functions:
- Request the compiler to replace the call to function with the block of code
- For small function body will increase instruction cache hit rate.
- Types of inline request
- implicit: define the function within the class
class Person { public: ... int age() const { return theAge; } // an implicit request }
- explicit: using
inline
keyword before the function.
- implicit: define the function within the class
Caveats:
- Constructors and destructors might seem like good inline functions but under the hood, they are long functions that have code to construct the data members
- How the function is called matters: Function pointer to inline function would
cause the inline function to be generated out of line.
inline void f() {...} void (*pf)() = f; ... f(); // inlined pf(); // most probably wont be inlined
- DLL: if a library uses inline function, the client must recompile the library instead of just relinking a non-inline function
Item 31: Minimize compilation dependencies between files
Problem:
- There could be a cascading compilation dependencies when the declaration and definitions are
coupled together.
- A client only cares about the public functions and not the private data members.
- Coupling the declaration and definitions (private data members) would result in the client
having to recompile when there is a change in private data member’s code.
#include <string> #include "date.h" #include "address.h" class Person { public: Person(const std::string& name, const Date& birtday, const Address& addr); std::string name() const; std::string birthDate() const; private: std::string theName; Date theBirthDate; Address theAddress }
Solution:
- pimpl
- Only declares functions and no data members
- Implement by forwarding the function call to the
impl
- Forward declare types
- Forward declare custom type so that if the client doesn’t need the custom type, they do not need to depend on it.
- If the client need the custom type they will just include the custom type header file instead of relying on your header file.
- Separate forward declaration to
*fwd.h
to a separate header file that just forward declares.
- interface
- Having an abstract base class that does not contain any data members. Having a pointer to abstract class with factory function will allow the client not having to include unneccassary dependencies.
Chapter 6: Inheritance and Object-Oriented Design
Item 32: Make sure public inheritance models “is-a”
Public Inheritance: the derived class will have access to all public members (function and data) of the base class. The derived class does not have access to private data members (need to access through public function of base class).
Take aways: All derived class should “is-a” base class. This would allow the compiler to prevent the derived class from having properties it should not have at compile time.
Item 33: Avoid hiding inherited names
Unlike other PL like Java, overriding a function would hide all functions in the base class with the same name (does not distinguish between function signature)
class Base {
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
};
class Derived: public Base {
public:
virtual void mf1();
void mf3();
void mf4();
}
Derived d;
int x;
d.mf1(); // calls Derived::mf1
d.mf1(x); // error! Base class mf1(int) got hidden
Rationale: C++ chose to hide all function names irregardless of function signature as it
Solution: use using
class Derived: public Base {
public:
using Base::mf1;
using Base::mf3;
virtual void mf1();
void mf3();
void mf4();
}
- Use this method if you inherit a base class and you want to redeclare or override some but not all functions
Forward functions
class Derived : private Base {
public:
virtual void mf1()
{ Base::mf1(); }
}
Item 35: Consider alternative to virtual functions
This item mainly talks about alternate design pattern we could use to acheive polymorphism over virtual functions. Aka Non-Virtual Interface (NVI) Idiom.
Template Method Pattern
This pattern is not related to c++’s template programming. The essence of this pattern is to declare all virtual function private and create a public wrapper function that would call the virtual functions.
As virtual function are private in base class, derived class can only redefine the function but cannot call it.
Advantage: allows the base class to inject context into the call of virtual functions. Ie perform before virtual call tasks (lock mutex) or after virtual call task (unlock mutex)
class GameCharacter {
public:
int healthValue() const
{
// perform before task
int ret = doHealthVal();
// peform after task
return ret;
}
private:
virtual int doHealthValue() const {
...
}
};
Strategy Pattern via std::function
For polymorphism that is independent of the class (ie health is independent of GameCharacter
),
have a member variable that stores a call back function to execute the polymorphic task.
The constructor or member function of the class will take in std::function
as an argument
and have another member function that wraps around the call back function.
Advantage: separate the health calculation and game character logic. Allow for change in polymorphic function in runtime.
class GameCharacter;
int defaultHealthCalc(ocnst GameCharacter& gc);
class GameCharacter {
public:
typedef std::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc) : healthFunc(hcf) {}
int healthVal() const
{ return healthFunc(*this); }
};
Item 36: Never redefine an inherited non-virtual function
This item argues that a derived class should never redefine a non-virtual function from a base class.
Practical Reason: non-virtual functions are statically bound. This means that
the correct non-virtual function to be invoked is determined statically (compile time?)
by the Type of the object/pointer. This would mean that the non-virtual
function for a derived
class could behave differently when it is called directly to the object or type cast to a reference
of the base class. Ie
clas B {
public:
void mf();
};
class D : public B {
public:
void mf();
};
D x;
B *pB = &x;
D *pD = &x;
pB->mf(); // calls B::mf()
pD->mf(); // calls D::mf()
Theoretical reason: If a derived class is needs to redefine a non-virtual function, this means that the derived class no longer is-a base class and violates Item 32 principles.
Item 37: Never redefine a function’s inherited default parameter value.
Definitions:
- Static Type: The type you declare it in the program
- Static Bound: The property of the object is determined by the static type
- Dynamic Type: The type of object it currently refers to
- Dynamic Bound: The property of the object(function/default param) is determined from the dynamic type
Problem: default parameters are statically typed.
This means that if the derived class default parameter differs from the base class default parameter, calling the virtual function on a derived class cast as base class, the derived class function would be invoked but with the base class default value.
class Shape {
enum ShapeColor {Red, Green, Blue};
virtual void draw(ShapeColor color = Red) const = 0;
};
class Rectangle : public Shape {
enum ShapeColor {Red, Green, Blue};
virtual void draw(ShapeColor color = Green) const;
};
Shape *pr = new Rectangle;
pr->draw(); // Invokes Rectangle::call(Shape::Red);
Why c++ implement this: C++ chose to implement this behaviour to allow for runtime efficiency. At runtime you do not need to find what polymorphic default parameter to use.
Solution
- All class basee and derived class use the same default parameter.
- This is bad as there would be a lot of code duplication and dependencies. Changing the default value in one class would result in all classes default value being changed
- Use NVI idiom. Wrap the virtual function with a non-virtual function and the non-virtual function can define a default parameter that would be invoked for all classes’ virtual function.
Item 38: Model “has-a” or “is-implemented-in-terms-of” through composition
Composition is a very technique any SWE uses when designing an object. This item provides an interesting mental model on when to use composition. I have never heard of this technique before and it seems pretty good.
Definitions:
- Application domain: objects that corresponds to things in a real world (ie people, vehicles and etc)
- Implementation domain: purely implementation objects (mutex, buffers, etc)
When to use composition:
- has-a relationship in application domain should use composition over inheritance. Person has a name data member instead of inheriting from name
- is-implemented-in-terms-of relationship in implementation domain should use composition. When implementing a set with set with a underlying linked list data structure, you do not inherit a linked list but instead have a linked list data member and member functions that calls linked list functions.
Item 39: Use private inheritance judiciously
This item cover the subtle but important characteristics about private inheritance. I was always under the impression that the purpose of private inheritance was solely for access to private data members.
Private inheritance behaviour:
- Compiler does not convert private derived class to base class
class B {}; class D : private B{}; void foo(B* ptr); ... D* d = new D; foo(d); // compilation error
- All members (data and functions) inherited from private base class becomes private members of the derived class
Purpose of private inheritance: Another form of “is-implemented-in-terms-of” implementation.
- All public interface of private base becomes private members of derived means that derived “is-not-a” base class
- Unlike composition in Item 38, private inheritance allows derived class to have access to the private data member of the underlying implementation of. This allows for more flexibility if you need the underlying implementation of data.
- Allow for redefining virtual function of the implement of base class.
- Unlike composition, private inheritance allow for empty class optimisation.
- If a derived class privately inherit an empty base class, there would not be any additional space overhead.
Item 40: Use multiple inheritance judiciously
Unlike Java, C++ allows for multiple inheritance. However, there are a few benefits and drawbacks of multiple inheritance.
Ambiguity: When a derived class inherits from two different base class with the same member name. Note that there will still be ambiguity even if one of the member is accessible while the other is not. This is due to the compiler identifying the corresponding member before checking for access (similar to item 33).
Solution: Call the specify the correct base class member you would like to refer to. Ie
ie.BoroowableItem::checkOut()
instead of ie.checkOut()
Multiple Inheritance Diamond
When you inherit from two base class which in turn inherit from the same base class.
Problem: This would result in two separate instance of an ancestor class from a single class.
Solution: use virtual inheritance. This would result in only one instance of the common ancestor to be instantiated.
Multiple inheritance use case: Easily implement interface and utilise existing classes implementation (is-implementation-in-terms-of).
- Publicly inherit an abstract class (interface)
- Privately inherit a base class. Reuse the implementation of the base class.
Virtual Inheritance Drawbacks:
- There is an overhead when accessing data members of virtual base classes (details depends on compiler).
- Derived class needs to be aware of all virtual ancestor no matter how far away (additional overhead)
Drawbacks mitigation: Avoid adding data members to the virtual base class (similar to Java interface). This would prevent additional overhead of accessing virtual base class data members.
Chapter 7: Template and Generic Programming
Introduction Templates were initially created to allow for type-safe containers (vector
).
However, it has evolved to be a turing complete stack. The compile will execute and run
the metaprogram in compile time and complete the execution when the program exits.
Item 41: Understand implicit interfaces and compile time polymorphism.
Traditionally we use explicit interface and runtime polymorphism.
- Explicit interface:
- Interface of a class/struct.
- Users will be able to look up the type and view the function signature
- Runtime polymorphism:
- Only at runtime we will know what virtual function to call using virtual pointer.
Metaprogramming allows for implicit interface and compile time polymorphism.
How it works: at compile time, the compiler will take the function template and the template argument (the actual type to be supplied) and synthesize/instantiate a function with that type.
Implicit interface
Instead of having an explicit interface stating the member functions and variables. With templates, you will state the functions that you would like use for the generic type and the compiler will check if the supplied template at compile time support these functions (technically it should be expressions).
template<typename T>
void doProcessing(T& w) {
if(w.size() > 10 && w != someNastyWidge) {
T temp(w);
temp.normalize();
temp.swap();
}
}
- Compiler will only compile if
w
has.size(),
,.normalize()
and.swap()
Compile time polymorphism
At compile time, the compiler will synthesize the function template for that specific supplied type. This will let the correct function for each type to be called instead of relying on virtual functions.
For example:
Foo("ssss");
Foo(1234);
- The same function template (Foo) will be synthesized for string and int. It will call the respective int/string member function and thus allowing for polymorphism.
Item 42: Understand the two meaning of typename
I am still a bit confused with this item.
Different type
s in a template:
- dependent name: when a named type is dependent on a template type. ie
T::foo
- nested dependent name: a dependent type that is nested inside a class.
- non-dependent name: any type that is not dependent on the template type. ie
int
Problem with nested dependent: when using nested dependent type, we do not know if T::foo
is another
type or is a static variable. To solve this, use typename
at the front.
template<typename C>
void Foo(const C& cont) {
typename C::const_iterator iter;
}
Item 43: Know how to access names in templatized base class
Problem:
- When a template class inherits a base template class, the compiler will not let you call the base class function in the derived class.
- Why: the compiler prevents this as the base template class could have a total specialisation which could result in the base class having a completely different interface.
Solution:
- Call the base class function with
this->
- Employ
using
declaration technique.using Base<T>::foo;
- Call the base class function directly
Base::foo(...)
. Not ideal as it would not work for virtual functions.
Note: If there is full template specialisation that removes the function and the techniques above were used, there will compilation error. However, this would occur later when the template is synthesized.
Item 44: Factor parameter-independent code out of templates
I do not really understand the examples for the this item but I believe I understand the main takeaways of it.
All templates in C++ are just templates for functions/class. Thus, having more than necessary template parameters could result in more than necessary synthesized template class or functions.
Refactor out any template parameter that are independent of the code (usually non types parameters) and pass the template parameter as a function parameter instead. This will allow lesser synthesisation of templates that just differ by that parameter.
Item 45: Use member function template to accept “all compatible types”
Problem: If you would like to cast a template class of type U
to the same template class
but of type T
.
Solution: Use a member function that takes in any type. This would allow you to perform implicit
type conversion from U
to T
inside the template class.
template<class T>
class shared_ptr {
public:
shared_ptr(shared_ptr const& r);
template<class Y>
shared_ptr(shared_ptr<Y> const& r); // allow for implicit type conversion
shared_ptr& operator=(shared_ptr const& r); // copy assignment
template<class Y>
shared_ptr& operator=(shared_ptr<Y> const& r); // copy assignment
}
Note: You will need to declare the normal copy constructor and assignment to allow as the compiler can still create its own constructors
Item 46: Define non-member functions inside templates when type conversion are desired.
If a function needs to take 2 parameter with the same template type but the 2 provided arguments do not have the same type will lead to compilation error.
Why error: Unlike normal functions, template arguments deduction never perform implicit type conversion. The function does not know which type to keep and which type to convert
Solution: use friend function to set the first argument as the main type and typecast the others which would call the template function with the same types now.
template<typename T>
const Rationale<T> doMultiply(const Rationale<T>& lhs, const Rationale<T>& rhs) {
return Rationale<T>(lhs.numerator* rhs.numerator, lhs.denomerator*rhs.denomerator);
}
template<typename T>
class Rationale {
// rhs will be cast to type T
friend const Rationale operator*(const Rationale& lhs, const Rationale& rhs) {
doMultiply(lhs, rhs)
}
};
Item 47: Use traits classes for information about types
(klement: Revisited this item a few months later after initially reading it. I did not understand this item fully the previous time.)
Problem: when different types with the same API should be handled differently in a template function.
- Example:
std::advance
for the different types of iterators
Solutions: use traits to get the tag for the different types and from the tag, use tag dispatch to invoke the correct overloaded function.
Traits:
- Requirements: need to be able to extract the traits for all types (built in and user defined)
- Nesting the trait within a built in type is not possible
- Use Trait Classes (actually is a template struct)
- For user-defined type, the default Trait Class will just echo back the nested typedef of the user defined type
template<typename IterT> struct iterator_traits { // Echo back the nested iterator_category typedef typedef typename IterT::iterator_category iterator_category; }
- Calling
iterator_traits<dequeu::iterator>::iterator_category
will return theiterator_category
defined indequeu::iterator_category
- Calling
- For builtin types, there is partial/full Trait Class specialisation that
will define a specialised template
template<typename T> struct iterator_traits<T*> { typedef randome_access_iterator_tag iterator_category; }
- Calling
iterator_traits<int*>::iterator_category
will return theiterator_category
defined in partial template specialisation instead
- Calling
- For user-defined type, the default Trait Class will just echo back the nested typedef of the user defined type
Tag dispatch:
- Using Trait Class we will be able to get the tag for each type and use overload resolution to invoke the correct class
- Iterators example:
strct input_iterator_tag {}
struct output_iterator_tag {}
struct forward_iterator_tag: public input_iterator_tag {}
struct bidirectional_tag: public forward_iterator_tag {}
struct random_access_iterator_tag: public bidirectional_tag {}
- Iterators example:
- Overloaded function that takes in an extra Tag parameter that will help with the overload resolution
- ie
void doAdvance(Iter& iter, Dist d, std::random_access_iterator_tag)
,void doAdvance(Iter& iter, Dist d, std::bidirectional_tag)
- The tag parameter is an empty class that is used solely for overload resolution
- ie
- Tags can be polymorphic to allow for the polymorphism in the tag dispatch
- If some types have inheritance nature (ie forward iterator is a input iterator), there is not need to define two overloaded function for the parent and child type. Instead the compiler will automatically invoke the parent tagged function if the child tagged function is not present
Chapter 8 customizing new
and delete
C++ allows you to define a callback function (new_handler
) to be executed when it fails to
allocate new memory.
Item 49: Understand the behaviour of new-handler
How it works
- Declare custom
new_handler
. This would apply to allnew
operations.set_new_handler
returns the previousnew_handler
.std::set_new_handler(outOfMem);
- When
new
is called- Sufficient memory: nothing happens and the memory gets allocated
- Insufficient memory:
- Keep calling the latest function of
set_new_handler(...)
until there is an exception or sufficient memory allocated
As new
will be recursively called, new_handler
should have one of the following properties:
- Make more memory available: Next
new
call would have sufficient memory - Install a different new-handler: Will not call the same function again and stuck in infinite loop
- Deinstall new-handler: set
set_new_handler(nullptr)
so that no function would be called and will use the default exception - Throw an exception: of type
bad_alloc
- Not return: call abort or exit
Class specific new handler
Challenges:
- Need to call
set_new_handler(fooHanlder)
andset_new_handler(oldHandler)
after the new operation - Will need to handle when there is an exception when calling
new
Solution:
- Treat
new_handler
as a resource and wrap it in a RAII. - Use Curiously Recurring Template Pattern (CRTP) to make a mixin base class that could be inheritted by multiple classes
- (Klement: I do not really understand the solution proposed in the book but you can refer to it if you would to view the actual implementation)
Item 50: Understand when it makes sense to replace new
and delete
This item lays down a few use cases for why we should use a custom new
and delete
Use Cases:
- Detect usage errors:
- Custom
new
anddelete
can keep a list ofnew
ed anddelete
d memories. This would let you detect if delete an address from outside the list - Over allocate blocks so there is room to put signature before and after the memory. When you call delete, check the signature are still intact.
- Caveats: alignment of memory is very important. On some systems, pointers need to be four-byte aligned and failure to do so could result in hardware
exception. On other systems(Intel x86) doubles that are eight-byte aligned can be accessed much faster. Malloc will return pointers that are properly aligned
but adding the signature before might misalign it.
static cosnt int signature = 0xDEADBEEF; typedef usgined char Byte; void* operator new(std::stize_t size) throws(std::bad_alloc) { using namespace std; size_t realSize = size + 2 * sizeof(int); void *pMem = malloc(realSize); if (!pMem) throw bad_alloc(); *(static_cast<int*>(pMem)) = signature; *(reinterpret_cast<int*>(static_cast<Byte*>(pMem) + realSize - sizof(int))) = signature; return static_cast<Byte*>(pMem) + sizeof(int); }
- Custom
- Improve efficiency:
- Default
new
need to worry about heap fragmentation (pocket of unused spaces within the heap). - Writing a thread-unsafe
new
anddelete
for single threaded systems.
- Default
- Collect usage statistics:
- Allow you to collect all kind of stats for
new
anddelete
usage
- Allow you to collect all kind of stats for
- Cluster related objects near one another:
- If certain objects are used closely with one another, you can allocate them near each other to utilise the cache spatial locality.
Item 51: adhere to convention when writing new and delete
Requirements:
- right return value (pointer)
- If failed to allocate memory keep calling the new-handling-function(item 49) if present else throw
bad_alloc
- Return a legitimate pointer if zero bytes requrested.
Caveats
- Handle when the derived class calls the base class custom operator
new
class Base { public: static void operator new(std::size_t size) throw(std::bad_alloc) {...} class Derived : public Base { ... }; Derived* p = new Dervied // calls Base::operator new }
- If you want to customise the operator
new
for array, you will need write a customoperator new[]
- You do not know the size and number of each element.
Base[]
might storeDerived
size_t
supplied might be more than required memory for all elements as dynamically allocated array might add meta data (item 16)
- You do not know the size and number of each element.
Item 52: Write placement delete
if you write placement new
Definition placement new and delete: When new
is called it usually
calls the static void* operator new(std::size_t size)
member function.
However, we can declare our own new operator that accepts more than size_t
parameter.
- ie
static void* operator new(size_t size, ostream& logstream)
Placement new with address:
static void* operator new(size_t size, void* ptr)
- Allow you to construct an object on the given address instead of allocating new memory
- Used in vectors
Item 53: Pay attention to compiler warnings
This item encourages you to pay attention to compiler warnings.
Item 54: Familiarize yourself with the standard library, including TR1.
This item briefly covers the useful standard libraries. However, the segment on TR1 is not very relevant as the majority of TR1 is already in the c++ standards.
Item 55: Familiarize yourself with boost
Briefly covers the different features of boost but I think the best way for you to familiarise yourself with boost is to use it in a project.