Key C++ Programming Guidelines from Effective C++

Understanding C++ as a Language Federation

C++ is a multi-paradigm programming language supporting procedural, object-orianted, functional, generic, and metaprogramming paradigms. Understanding C++ involves recognizing four distinct language subdomains:

C: The C subset provides limitations—templates, exceptions, and function overloading are unavailable. In C, pass-by-value is recommended, whereas C++ favors pass-by-reference.

Object-Oriented C++: This adds classes with constructors and destructors, encapsulation, inheritance, polymorphism, and virtual functions with dynamic binding.

Template C++: Generic programming enables powerful template metaprogramming (TMP) techniques.

STL: The Standard Template Library provides containers, iterators, algorithms, and function objects following consistent conventions.


Item 1: Prefer explicit Constructors

Unless a class constructor is intended for implicit type conversion, declare it explicit. This prevents unintended conversions:

class Asset {
public:
    explicit Asset(int value = 0, bool taxable = true);
};

void process(Asset target);

Asset a1;
process(a1);           // OK
process(42);            // Error: no implicit conversion
process(Asset(42));    // OK: explicit construction

Item 2: Copy Constructor vs Copy Assignment

When initializing or assigning objects, C++ uses different mechanisms:

  • New object definition (Widget w3 = w2): calls the copy constructor
  • Existing object assignment (w1 = w2): calls the copy assignment operator
class Widget {
public:
    Widget();                        // default constructor
    Widget(const Widget& source);   // copy constructor
    Widget& operator=(const Widget& source); // copy assignment
};

Widget w1;            // default constructor
Widget w2(w1);        // copy constructor
w1 = w2;              // copy assignment
Widget w3 = w2;       // copy constructor (not assignment!)

Item 3: Pass by Const Reference

Avoid passing user-defined types by value to copy constructors. Prefer const references for efficiency:

// Instead of this:
void process(std::string s);  // Copy occurs

// Use this:
void process(const std::string& s);  // No copy, const guaranteed

Item 4: Avoid Undefined Behavior

Two primary sources of undefined behavior:

  • Dereferencing null pointers
  • Accessing arrays with invalid indices

Item 5: Character Array Terminator

Remember the null terminator when working with character arrays:

char name[] = "Darla";  // Length is 6, including '\0'

Item 6: Naming Conventions

Left and Right Hand Side: Use lhs and rhs for binary operators. Member functions use rhs since the left operand is this:

class Rational {
public:
    Rational operator*(const Rational& rhs) const;  // member
};
const Rational operator*(const Rational& lhs, const Rational& rhs);  // non-member

Pointers and References: Name pointers as pt (pointer to T) and references as rt (reference to T):

class Airplane;
Airplane* planePtr;           // pointer to Airplane
Airplane& planeRef = *planePtr; // reference to Airplane

Item 7: Replace #define with Const, Enum, or Inline

Using const for Constants

Preprocessor macros bypass the compiler and don't appear in symbol tables:

// Avoid:
#define ASPECT_RATIO 1.653

// Prefer:
const double AspectRatio = 1.653;

The constant version is visible to the compiler and avoids code duplication from naive macro substitution.

Constant Pointers: Declare both levels of const when defining pointers to constants in headers:

const char* const authorName = "Scott Meyers";
// Or prefer std::string:
const std::string authorName("Scott Meyers");

Class-Scoped Constants: Use static members for class-specific constants:

class GamePlayer {
private:
    static const int NumTurns = 5;  // declaration with initializer
    int scores[NumTurns];
};

If you need the address, provide a definition in the implementation file:

const int GamePlayer::NumTurns;  // definition, no initializer

Using enum for Constants

When a compile-time constant is needed inside a class (such as array bounds) and your compiler doesn't support in-class static integer constant initialization:

class GamePlayer {
private:
    enum { NumTurns = 5 };  // compile-time constant
    int scores[NumTurns];
};

Enumerations behave like #define without causing unnecessary memory allocation, and pointers cannot be taken to them.

Using inline for Macro-like Functions

Avoid macro functions that simulate function calls without the overhead:

// Dangerous macro:
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))

int x = 5, y = 0;
CALL_WITH_MAX(++x, y);  // x incremented twice!

Template inline functions provide type safety and predictable behavior:

template<typename T>
inline T& callWithMax(const T& a, const T& b) {
    return a > b ? a : b;
}

Item 8: Use Const Whenever Possible

Const Fundamentals

const on * left side means the pointed-to object is const; on the right side, the pointer itself is const:

char greeting[] = "Hello";
char* p1 = greeting;              // non-const pointer, non-const data
const char* p2 = greeting;         // non-const pointer, const data
char* const p3 = greeting;         // const pointer, non-const data
const char* const p4 = greeting;  // both const

Const with STL Iterators

std::vector<int> data;

// Similar to T* const:
std::vector<int>::iterator iter = data.begin();
*iter = 10;   // OK
++iter;       // Error: iterator is const

// Similar to const T*:
std::vector<int>::const_iterator cIter = data.begin();
*cIter = 10;  // Error: data is const
++cIter;      // OK

Const in Function Declarations

Returning const from operator functions prevents accidental assignments:

class Rational { /* ... */ };
const Rational operator*(const Rational& lhs, const Rational& rhs);

Rational a, b, c;
if ((a * b) = c)  // Error caught by compiler
    /* ... */;

Without the const return type, this typo in a condition would compile and exhibit undefined behavior.

Const Member Functions

Const member functions enable operations on const objects and clarify which functions modify state:

class TextBlock {
public:
    const char& operator[](std::size_t pos) const { return text[pos]; }
    char& operator[](std::size_t pos) { return text[pos]; }
private:
    std::string text;
};

This overloading allows different return types based on const-ness.

Bitwise vs Logical Constness: Compilers enforce bitwise constness (no modifications to any non-static member), but some logically const operations require mutable:

class TextBlock {
public:
    std::size_t length() const;
private:
    char* pText;
    mutable std::size_t cachedLength;
    mutable bool lengthValid;
};

std::size_t TextBlock::length() const {
    if (!lengthValid) {
        cachedLength = std::strlen(pText);
        lengthValid = true;
    }
    return cachedLength;
}

Avoiding Code Duplication Between Const and Non-Const Members

Have the non-const version call the const version and remove const via const_cast:

class TextBlock {
public:
    const char& operator[](std::size_t pos) const {
        // perform bounds checking
        return text[pos];
    }
    char& operator[](std::size_t pos) {
        return const_cast<char&>(
            static_cast<const TextBlock&>(*this)[pos]
        );
    }
};

Item 9: Initialize Objects Before Use

Constructor Initialization

Uninitialized members cause undefined behavior. Always use member initializer lists:

class Point {
public:
    Point(int xCoord, int yCoord);
private:
    int x, y;
};

// Efficient: direct construction
Point::Point(int xCoord, int yCoord)
    : x(xCoord), y(yCoord) {}

// Inefficient: default construction followed by assignment
Point::Point(int xCoord, int yCoord) {
    x = xCoord;
    y = yCoord;
}

Member initializers are executed before the constructor body, avoiding the overhead of default construction plus assignment.

Initialization Order

C++ initializes base classes before derived classes, and members in declaration order. Always write the initializer list in declaration order:

class Entity {
private:
    std::string name;  // declared first
    int id;              // declared second
public:
    Entity(const std::string& n, int i)
        : name(n),   // OK: matches declaration order
          id(i)      // OK
    {}
};

Non-Local Static Object Initialization

Static objects have lifetime from construction until program termination. Initialization order across translation units is undefined. Use local static objects in accessor functions to ensure proper initialization order:

class FileSystem {
public:
    std::size_t diskCount() const;
};

FileSystem& getFileSystem() {
    static FileSystem instance;
    return instance;
}


class Directory {
public:
    Directory(params);
private:
    std::size_t diskCount = getFileSystem().diskCount();
};

Item 10: Compiler-Generated Functions

Empty classes implicitly receive public inline versions of:

class Empty {};

// Equivalent to:
class Empty {
public:
    Empty();                              // default constructor
    Empty(const Empty& source);             // copy constructor
    ~Empty();                              // destructor
    Empty& operator=(const Empty& source); // copy assignment
};

Important cases:

  • Declaring any constructor prevents default constructor generation
  • Classes with reference or const members must define their own copy assignment
  • Base classes with private or deleted copy assignment prevent derived class generation

Item 11: Explicitly Disable Unwanted Compiler Functions

To prevent copying, declare copy constructor and copy assignment as private without implementation:

class DoNotCopy {
private:
    DoNotCopy(const DoNotCopy&);           // no implementation
    DoNotCopy& operator=(const DoNotCopy&);
};

This causes linker errors if copying is attempted. For earlier detection, create a base class:

class Uncopyable {
protected:
    Uncopyable() {}
    ~Uncopyable() {}
private:
    Uncopyable(const Uncopyable&);
    Uncopyable& operator=(const Uncopyable&);
};


class MyClass : private Uncopyable { /* ... */ };

Attempting to copy MyClass now produces compilation errors.


Item 12: Polymorphic Base Classes Need Virtual Destructors

Without a virtual destructor, deleting a derived object through a base pointer causes undefined behavior—only the base portion is destroyed:

class TimeKeeper {
public:
    virtual ~TimeKeeper();  // virtual destructor
};

class AtomicClock : public TimeKeeper { /* ... */ };


TimeKeeper* ptk = new AtomicClock;
delete ptk;  // Now properly destroys AtomicClock, then TimeKeeper

Classes without virtual functions typically aren't base classes. Avoid making such classes polymorphic.

Pure Virtual Destructors: Pure virtual destructors still require implementation:

class AbstractBase {
public:
    virtual ~AbstractBase() = 0;
};
AbstractBase::~AbstractBase() {}

Item 13: Never Let Exceptions Escape Destructors

Destructors that throw exceptions risk undefined behavior. Handle exceptions within destructors:

class DatabaseConnection {
public:
    static DatabaseConnection create();
    void close();
};

class Connection {
public:
    ~Connection() {
        try {
            db.close();
        } catch (...) {
            // log error
        }
    }
private:
    DatabaseConnection db;
};

Alternatively, provide an explicit close function and let users handle exceptions:

class Connection {
public:
    void close() {
        db.close();
        closed = true;
    }
    ~Connection() {
        if (!closed) {
            try { db.close(); }
            catch (...) { /* log */ }
        }
    }
private:
    DatabaseConnection db;
    bool closed = false;
};

Item 14: Avoid Virtual Function Calls in Constructors and Destructors

During base class construction, virtual functions resolve to the base class version, not derived:

class Transaction {
public:
    Transaction(const std::string& logInfo) {
        logTransaction(logInfo);  // calls base version
    }
    virtual void logTransaction(const std::string&) const {}
};


class BuyTransaction : public Transaction {
public:
    BuyTransaction(params)
        : Transaction(createLogString(params)) { /* uses base log */ }
    virtual void logTransaction(const std::string&) const override;
private:
    static std::string createLogString(params);  // static to avoid uninitialized members
};

The same applies to destructors—after derived destructors complete, the object behaves as the base type.


Item 15: Return References from Assignment Operators

Assignment should support chaining and return a reference to the left operand:

class Widget {
public:
    Widget& operator=(const Widget& rhs) {
        return *this;
    }
    Widget& operator+=(const Widget& rhs) {
        return *this;
    }
};

int x, y, z;
x = y = z = 15;  // chained assignment

Item 16: Handle Self-Assignment in Assignment Operators

Self-assignment occurs when objects share storage or aliases exist:

// Unsafe without self-assignment check:
Widget& Widget::operator=(const Widget& rhs) {
    delete bitmap;          // Deletes rhs's bitmap if this == rhs!
    bitmap = new Bitmap(*rhs.bitmap);
    return *this;
}

// Safe version:
Widget& Widget::operator=(const Widget& rhs) {
    if (this == &rhs) return *this;
    delete bitmap;
    bitmap = new Bitmap(*rhs.bitmap);
    return *this;
}

A better approach preserves original state until new allocation succeeds:

Widget& Widget::operator=(const Widget& rhs) {
    Bitmap* original = bitmap;
    bitmap = new Bitmap(*rhs.bitmap);
    delete original;  // Only deletes after successful allocation
    return *this;
}

Copy-and-swap provides both self-assignment safety and strong exception safety:

Widget& Widget::operator=(Widget rhs) {  // pass by value for copy
    swap(rhs);
    return *this;
}

Item 17: Copy All Parts of an Object

When defining copy constructors and copy assignment operators, ensure all members are copied:

class Customer {
private:
    std::string name;
    Date lastTransaction;
};

Customer::Customer(const Customer& rhs)
    : name(rhs.name),
      lastTransaction(rhs.lastTransaction)  // Don't forget!
{}

Customer& Customer::operator=(const Customer& rhs) {
    name = rhs.name;
    lastTransaction = rhs.lastTransaction;  // Don't forget!
    return *this;
}

With Inheritance: Always invoke base class copy operations:

class PriorityCustomer : public Customer {
public:
    PriorityCustomer(const PriorityCustomer& rhs)
        : Customer(rhs),  // Call base copy constructor
          priority(rhs.priority) {}
    PriorityCustomer& operator=(const PriorityCustomer& rhs) {
        Customer::operator=(rhs);  // Call base copy assignment
        priority = rhs.priority;
        return *this;
    }
};

Item 18: Use Objects to Manage Resources (RAII)

Resource Acquisition Is Initialization (RAII) ensures resources are released when objects are destroyed:

void compute() {
    auto_ptr<int> result(new int(42));
    // auto_ptr automatically deletes on exit
}

auto_ptr transfers ownership on copy—copied pointer becomes null. shared_ptr allows shared ownership:

void process(std::shared_ptr<int> result) {
    // shared ownership
}

std::shared_ptr<int> data(new int(42));
process(data);  // safe copy

Note: Both auto_ptr and shared_ptr use delete, not delete[]. Use containers for dynamic arrays.


Item 19: Handle Copying in Resource-Managing Classes

Decide how your RAII class copies its managed resource:

Approach 1: Prohibit copying

class Lock : private Uncopyable {
public:
    explicit Lock(Mutex* m) : mutex(m) { lock(mutex); }
    ~Lock() { unlock(mutex); }
private:
    Mutex* mutex;
};

Approach 2: Reference counting with shared_ptr

class Lock {
public:
    explicit Lock(Mutex* m)
        : mutex(m, unlock) {  // custom deleter
            lock(mutex.get());
        }
private:
    std::shared_ptr<Mutex> mutex;
};

shared_ptr calls the deleter when the reference count reaches zero.


Item 20: Provide Access to Raw Resources in RAII Classes

APIs may require raw pointers. Both explicit and implicit access are possible:

Explicit via get()

std::shared_ptr<Resource> res(new Resource);
process(res.get());  // explicit get()

Implicit via operator overloading

class Font {
public:
    operator FontHandle() const { return handle; }  // implicit conversion
};

Font f(getFont());
changeFontSize(f, 12);  // Font automatically converted

Explicit access (get()) prevents accidental conversions and is generally safer.


Item 21: Match new with delete Consistently

When using new with [], always use delete[]:

std::string* s1 = new std::string;   // single object
std::string* s2 = new std::string[100];  // array
delete s1;     // correct
delete[] s2;   // correct

typedef Warning:

typedef double Values[4];
double* v = new Values;
delete v;      // wrong!
delete[] v;    // correct

Item 22: Store newed Objects in Smart Pointers Immediately

Passing raw pointers to functions creates risks if excepsions occur between allocation and smart pointer construction:

// Risky: exception between new and shared_ptr construction
process(std::shared_ptr<Widget>(new Widget), computePriority());


// Safe: separate statements
std::shared_ptr<Widget> pw(new Widget);
process(pw, computePriority());

Item 23: Design Interfaces for Ease of Use

Prevent misuse by using strong typing and validation:

struct Month {
    explicit Month(int m) : value(m) {}
    int value;
};

struct Day {
    explicit Day(int d) : value(d) {}
    int value;
};

struct Year {
    explicit Year(int y) : value(y) {}
    int value;
};

class Date {
public:
    Date(const Month& m, const Day& d, const Year& y);
};

Date d(Month::Mar(), Day(30), Year(1995));

Prevent invalid values at compile time:

class Month {
public:
    static Month Jan() { return Month(1); }
    static Month Dec() { return Month(12); }
private:
    explicit Month(int m);
};

Item 24: Treat Classes as Types

Design classes considering:

  1. Object creation and destruction

  2. Initialization vs assignment semantics

  3. Pass-by-value parameter conventions

  4. Type conversion requirements

  5. Inheritance relationship

  6. Required operators and functions

  7. Privacy requirements

  8. Whether to template

  9. Need for the new type at all


Item 25: Pass by Const Reference Instead of Value

Passing by value copies the entire object plus all subobjects. Pass by const reference avoids copies and prevents slicing:

class Base { virtual void draw() const {} };
class Derived : public Base { void draw() const override {} };

// Pass by reference to avoid slicing:
void render(const Base& obj) { obj.draw(); }  // calls Derived::draw()
void render(Base obj) { obj.draw(); }        // calls Base::draw()

Item 26: Avoid Returning References to Local Objects

Local stack objects are destroyed when functions return. Heap allocation requires matching delete. Static objects persist across calls but cause issues with comparison:

Rational operator*(const Rational& a, const Rational& b) {
    static Rational result;
    result = /* ... */;
    return result;  // all instances share the same static!
}

// Problem:
if ((a * b) == (c * d))  // always true!

Return by value and let the compiler optimize return value optimization (RVO).


Item 27: Declare Members Private

Member variables should only be accessible through functions (getters/setters). This maintains encapsulation:

class Point {
private:
    int x, y;
public:
    int getX() const { return x; }
    int getY() const { return y; }
};

Private members provide stronger encapsulation than protected, since protected members can be accessed by any derived class.


Item 28: Prefer Non-Member Functions for Greater Encapsulation

Non-member, non-friend functions don't increase access to private members:

namespace MathUtils {
    class Rational { /* ... */ };
    Rational operator*(const Rational& a, const Rational& b);  // non-member
}

This keeps the class interface minimal and allows the namespace to span multiplle headers.


Item 29: Provide Conversion Functions for All Arguments

Binary operators requiring type conversion on all arguments should be non-member functions:

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

const Rational operator*(const Rational& a, const Rational& b) {
    return Rational(a.numerator() * b.numerator(),
                     a.denominator() * b.denominator());
}

With member functions, only the left operand participates in implicit conversion:

a * 2;    // a.operator*(2) - OK
2 * a;    // 2.operator*(a) - Error, int has no operator*

Item 30: Implement Efficient Swap Functions

Default swap copies objects, which is inefficient when objects contain large data:

class WidgetImpl {
    std::vector<double> data;  // expensive to copy
};

class Widget {
    std::shared_ptr<WidgetImpl> pImpl;
public:
    void swap(Widget& other) {
        using std::swap;
        swap(pImpl, other.pImpl);
    }
};

namespace std {
    template<>
    void swap<Widget>(Widget& a, Widget& b) {
        a.swap(b);
    }
}

For class templates, provide a non-member swap in the same namespace:

namespace WidgetStuff {
    template<typename T>
    class Widget { /* ... */ };
    
    template<typename T>
    void swap(Widget<T>& a, Widget<T>& b) {
        a.swap(b);
    }
}

Tags: C++ Effective C++ Programming Guidelines Software Engineering Best Practices

Posted on Fri, 04 Sep 2026 16:54:23 +0000 by Nabster