C++ Language-Specific Rules - Part 2: Design & Implementation
Based on Scott Meyers' Effective C++ series and C++ Core Guidelines
Part of a multi-file C++ rules series:
- lang-cpp-basics.md - Formatting, language basics, constructors, RAII
- lang-cpp-design.md (this file) - Design, declarations, and implementations
- lang-cpp-advanced.md - OOP, templates, and advanced topics
- lang-cpp-modern.md - Modern C++ (C++11/14/17/20)
- lang-cpp-guidelines.md - C++ Core Guidelines
- lang-cpp-reference.md - Quick reference checklist
Chapter 4: Designs and Declarations
Item 18: Make Interfaces Easy to Use Correctly and Hard to Use Incorrectly
Principle: Good interfaces are easy to use correctly and hard to use incorrectly.
Techniques:
Use types to prevent errors:
// BAD - easy to swap parameters
Date(int month, int day, int year);
Date d(30, 3, 1995); // Oops! Meant March 30th
// GOOD - types prevent errors
struct Day { explicit Day(int d) : val(d) {} int val; };
struct Month { explicit Month(int m) : val(m) {} int val; };
struct Year { explicit Year(int y) : val(y) {} int val; };
class Date {
public:
Date(const Month& m, const Day& d, const Year& y);
};
Date d(Month(3), Day(30), Year(1995)); // Can't mix up order!
```text
**Restrict values with types:**
```cpp
class Month {
public:
static Month Jan() { return Month(1); }
static Month Feb() { return Month(2); }
// ...
private:
explicit Month(int m);
};
Date d(Month::Mar(), Day(30), Year(1995));
```text
**Be consistent with built-in types:**
- Follow conventions from standard types
- If a * b is legal, users expect b * a to work too
**Prevent resource leaks:**
```cpp
// BAD - can leak if exception between new and shared_ptr construction
std::shared_ptr<Investment> createInvestment() {
return std::shared_ptr<Investment>(new Stock);
}
// GOOD - use make_shared
std::shared_ptr<Investment> createInvestment() {
return std::make_shared<Stock>();
}
```text
### Item 19: Treat Class Design as Type Design
**Principle:** Designing a class is defining a new type. Consider these questions:
1. **How should objects be created and destroyed?**
- Constructors, destructors, memory allocation
2. **How should initialization differ from assignment?**
- Behavior of constructors vs. assignment operators
3. **What does it mean to pass objects by value?**
- Copy constructor defines pass-by-value
4. **What are the restrictions on legal values?**
- Invariants that must be maintained
5. **Does it fit into an inheritance graph?**
- Virtual or non-virtual destructor?
- Virtual functions?
6. **What type conversions are allowed?**
- Implicit vs. explicit conversions
- Conversion operators
7. **What operators and functions make sense?**
- Member vs. non-member functions
8. **What standard functions should be disallowed?**
- Declare private or delete
9. **Who should have access to members?**
- Public, protected, private
- Friends
10. **What is the "undeclared interface"?**
- Performance guarantees
- Exception safety
- Resource usage
11. **How general is it?**
- Should it be a template?
12. **Do you really need a new type?**
- Can you use existing types?
### Item 20: Prefer Pass-by-Reference-to-const to Pass-by-Value
**Principle:** For user-defined types, pass-by-reference-to-const is more efficient and avoids slicing.
```cpp
// BAD - copies entire object
bool validateStudent(Student s);
// GOOD - no copying, can't modify
bool validateStudent(const Student& s);
```text
**Reasons:**
**1. Efficiency - avoids copying:**
```cpp
class Person {
public:
Person();
virtual ~Person();
private:
std::string name;
std::string address;
};
class Student: public Person {
public:
Student();
~Student();
private:
std::string schoolName;
std::string schoolAddress;
};
// BAD - copies 6 strings total!
bool validateStudent(Student s);
// GOOD - no copying
bool validateStudent(const Student& s);
```text
**2. Prevents slicing problem:**
```cpp
class Window {
public:
virtual void display() const;
};
class WindowWithScrollBars: public Window {
public:
virtual void display() const;
};
// BAD - slices off derived part!
void printNameAndDisplay(Window w) {
w.display(); // Always calls Window::display!
}
// GOOD - polymorphism works
void printNameAndDisplay(const Window& w) {
w.display(); // Calls correct version
}
```text
**Exception:** Built-in types, STL iterators, and function objects are designed for pass-by-value:
```cpp
void process(int x); // OK - built-in type
void process(Iterator iter); // OK - STL iterator
```text
### Item 21: Don't Return a Reference When You Must Return an Object
**Principle:** Never return a pointer or reference to a local object, heap-allocated object that must be deleted, or local static when multiple such objects may be needed.
```cpp
// BAD - returns reference to local
const Rational& operator*(const Rational& lhs, const Rational& rhs) {
Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result; // DISASTER - returns reference to destroyed object!
}
// BAD - heap allocation, who deletes?
const Rational& operator*(const Rational& lhs, const Rational& rhs) {
Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return *result; // Who calls delete?
}
// BAD - static, fails with multiple objects
const Rational& operator*(const Rational& lhs, const Rational& rhs) {
static Rational result;
result = ...;
return result; // Fails: if ((a * b) == (c * d))
}
// GOOD - return by value (compiler can optimize with RVO/NRVO)
const Rational operator*(const Rational& lhs, const Rational& rhs) {
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}
```text
### Item 22: Declare Data Members Private
**Principles:**
**1. Syntactic consistency** - clients access everything through functions:
```cpp
class AccessLevels {
public:
int getReadOnly() const { return readOnly; }
void setReadWrite(int value) { readWrite = value; }
int getReadWrite() const { return readWrite; }
void setWriteOnly(int value) { writeOnly = value; }
private:
int noAccess; // No access
int readOnly; // Read-only access
int readWrite; // Read-write access
int writeOnly; // Write-only access
};
```text
**2. Fine-grained access control** - read-only, write-only, read-write, no access
**3. Encapsulation** - can change implementation without breaking clients:
```cpp
class SpeedDataCollection {
public:
void addValue(int speed);
double averageSoFar() const;
private:
// Can change implementation later!
// Option 1: store average
double average;
// Option 2: compute on demand
std::vector<int> speeds;
};
```text
**Protected is also not encapsulated:**
- Changing protected members breaks derived classes
- Protected is almost as unencapsulated as public
### Item 23: Prefer Non-member Non-friend Functions to Member Functions
**Principle:** Prefer non-member non-friend functions for better encapsulation.
```cpp
class WebBrowser {
public:
void clearCache();
void clearHistory();
void removeCookies();
};
// LESS encapsulated - member function
class WebBrowser {
public:
void clearEverything() { // Member has access to private data
clearCache();
clearHistory();
removeCookies();
}
};
// MORE encapsulated - non-member function
void clearBrowser(WebBrowser& wb) { // Can only access public interface
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
```text
**Why non-member is better:**
- More encapsulation (doesn't increase functions with access to private data)
- Greater packaging flexibility (can be in different headers)
- Increased extensibility (clients can add their own convenience functions)
**Common in C++ standard library:**
```cpp
namespace std {
template<typename T>
class vector { ... };
// Lots of non-member functions operating on vector
template<typename T>
void sort(vector<T>& v);
}
```text
### Item 24: Declare Non-member Functions When Type Conversions Should Apply to All Parameters
**Principle:** If you need implicit type conversions on all parameters (including `this`), make the function non-member.
```cpp
class Rational {
public:
Rational(int numerator = 0, int denominator = 1); // Not explicit - allows implicit conversion
int numerator() const;
int denominator() const;
private:
int n, d;
};
// BAD - member function, asymmetric behavior
class Rational {
public:
const Rational operator*(const Rational& rhs) const;
};
Rational oneHalf(1, 2);
Rational result = oneHalf * 2; // OK: oneHalf.operator*(2)
// 2 implicitly converted to Rational(2)
result = 2 * oneHalf; // ERROR! No way to convert this
// GOOD - non-member function, symmetric behavior
const Rational operator*(const Rational& lhs, const Rational& rhs) {
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
Rational result = oneHalf * 2; // OK: operator*(oneHalf, Rational(2))
result = 2 * oneHalf; // OK: operator*(Rational(2), oneHalf)
```text
### Item 25: Consider Support for a Non-throwing swap
**Principle:** Provide an efficient, non-throwing swap function for your types.
**Default std::swap:**
```cpp
namespace std {
template<typename T>
void swap(T& a, T& b) {
T temp(a); // Potentially expensive for some types
a = b;
b = temp;
}
}
```text
**For pimpl idiom:**
```cpp
class Widget {
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs) {
*pImpl = *(rhs.pImpl); // Copy the Impl object
}
void swap(Widget& other) {
using std::swap;
swap(pImpl, other.pImpl); // Just swap pointers - efficient!
}
private:
WidgetImpl* pImpl;
};
// Non-member swap that calls member swap
namespace std {
template<>
void swap<Widget>(Widget& a, Widget& b) {
a.swap(b);
}
}
```text
**For templates:**
```cpp
template<typename T>
class WidgetImpl { ... };
template<typename T>
class Widget {
public:
void swap(Widget& other) {
using std::swap;
swap(pImpl, other.pImpl);
}
private:
WidgetImpl<T>* pImpl;
};
// Can't partially specialize std::swap, so use own namespace
namespace WidgetStuff {
template<typename T>
class Widget { ... };
template<typename T>
void swap(Widget<T>& a, Widget<T>& b) { // Non-member in same namespace
a.swap(b);
}
}
```text
## Chapter 5: Implementations
### Item 26: Postpone Variable Definitions as Long as Possible
**Principle:** Define variables when you have initialization values, not before.
```cpp
// BAD - wastes construction/destruction if exception thrown
std::string encryptPassword(const std::string& password) {
std::string encrypted; // Default constructed here
if (password.length() < MinimumPasswordLength) {
throw logic_error("Password is too short"); // encrypted wasted
}
// ...
encrypted = ...; // Assignment, not initialization
return encrypted;
}
// BETTER - delay until really needed
std::string encryptPassword(const std::string& password) {
if (password.length() < MinimumPasswordLength) {
throw logic_error("Password is too short");
}
std::string encrypted; // Default constructed here
encrypted = ...;
return encrypted;
}
// BEST - initialize directly
std::string encryptPassword(const std::string& password) {
if (password.length() < MinimumPasswordLength) {
throw logic_error("Password is too short");
}
std::string encrypted(computeEncryption(password)); // Initialized!
return encrypted;
}
```text
**In loops:**
```cpp
// Approach A: define outside loop
Widget w;
for (int i = 0; i < n; ++i) {
w = some value dependent on i;
...
}
// Cost: 1 constructor + 1 destructor + n assignments
// Approach B: define inside loop
for (int i = 0; i < n; ++i) {
Widget w(some value dependent on i);
...
}
// Cost: n constructors + n destructors
// Prefer Approach B unless:
// 1. Assignment is less expensive than constructor/destructor pair
// 2. You're dealing with performance-sensitive code
```text
### Item 27: Minimize Casting
**Principle:** Avoid casts whenever possible. When necessary, use C++ style casts and hide them in functions.
**C++ cast syntax:**
```cpp
const_cast<T>(expression) // Cast away const
dynamic_cast<T>(expression) // Safe downcasting
reinterpret_cast<T>(expression) // Low-level casts (dangerous!)
static_cast<T>(expression) // Force implicit conversions
```text
**Why C++ style casts are better:**
- Easy to identify in code (searchable)
- More specific about intent
- Compiler can check more strictly
**Common mistake with casts:**
```cpp
class Window {
public:
virtual void onResize() { ... }
};
class SpecialWindow: public Window {
public:
virtual void onResize() {
static_cast<Window>(*this).onResize(); // WRONG! Calls on copy, not *this
... // Do SpecialWindow-specific stuff
}
};
// CORRECT way:
class SpecialWindow: public Window {
public:
virtual void onResize() {
Window::onResize(); // Calls on *this
...
}
};
```text
**dynamic_cast is usually slow:**
```cpp
// BAD - cascading dynamic_casts
class Window { ... };
class SpecialWindow1: public Window { ... };
class SpecialWindow2: public Window { ... };
typedef std::vector<std::shared_ptr<Window>> VPW;
VPW winPtrs;
for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter) {
if (SpecialWindow1* psw1 = dynamic_cast<SpecialWindow1*>(iter->get())) {
...
} else if (SpecialWindow2* psw2 = dynamic_cast<SpecialWindow2*>(iter->get())) {
...
}
}
// BETTER - use virtual functions
class Window {
public:
virtual void blink() { } // Default implementation does nothing
};
class SpecialWindow1: public Window {
public:
virtual void blink() { ... } // Custom blink
};
for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter) {
(*iter)->blink(); // Polymorphism!
}
```text
### Item 28: Avoid Returning Handles to Object Internals
**Principle:** Don't return handles (references, pointers, iterators) to internal data. It breaks encapsulation and can lead to dangling handles.
```cpp
class Point {
public:
Point(int x, int y);
void setX(int newVal);
void setY(int newVal);
};
struct RectData {
Point ulhc; // upper left-hand corner
Point lrhc; // lower right-hand corner
};
class Rectangle {
public:
// BAD - returns reference to internal data
Point& upperLeft() { return pData->ulhc; }
Point& lowerRight() { return pData->lrhc; }
private:
std::shared_ptr<RectData> pData;
};
// Problem 1: Breaks encapsulation
Point coord1(0, 0);
Point coord2(100, 100);
const Rectangle rec(coord1, coord2);
rec.upperLeft().setX(50); // rec is supposed to be const!
// BETTER - return const reference
class Rectangle {
public:
const Point& upperLeft() const { return pData->ulhc; }
const Point& lowerRight() const { return pData->lrhc; }
};
// Problem 2: Dangling handles
class GUIObject { ... };
const Rectangle boundingBox(const GUIObject& obj); // Returns temp by value
GUIObject* pgo;
const Point* pUpperLeft = &(boundingBox(*pgo).upperLeft()); // DANGER! Temp destroyed
// pUpperLeft now dangles!
// BEST - return by value
class Rectangle {
public:
Point upperLeft() const { return pData->ulhc; }
Point lowerRight() const { return pData->lrhc; }
};
```text
### Item 29: Strive for Exception-Safe Code
**Principle:** Exception-safe functions offer one of three guarantees:
1. **Basic guarantee**: If exception thrown, program is in valid state (no leaked resources, no corrupted data)
2. **Strong guarantee**: If exception thrown, program state unchanged (commit-or-rollback semantics)
3. **Nothrow guarantee**: Promise never to throw exceptions (declared `noexcept` in C++11)
```cpp
class PrettyMenu {
public:
void changeBackground(std::istream& imgSrc);
private:
Mutex mutex;
Image* bgImage;
int imageChanges;
};
// BAD - not exception-safe
void PrettyMenu::changeBackground(std::istream& imgSrc) {
lock(&mutex);
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc); // If throws, mutex not released, bgImage dangles
unlock(&mutex);
}
// BETTER - basic guarantee
void PrettyMenu::changeBackground(std::istream& imgSrc) {
Lock ml(&mutex); // RAII lock
delete bgImage;
++imageChanges; // Incremented even if new throws
bgImage = new Image(imgSrc);
}
// BEST - strong guarantee (copy-and-swap)
class PrettyMenu {
public:
void changeBackground(std::istream& imgSrc);
private:
Mutex mutex;
std::shared_ptr<Image> bgImage; // Smart pointer
int imageChanges;
};
struct PMImpl {
std::shared_ptr<Image> bgImage;
int imageChanges;
};
void PrettyMenu::changeBackground(std::istream& imgSrc) {
Lock ml(&mutex);
std::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); // Copy
pNew->bgImage.reset(new Image(imgSrc)); // Modify copy
++pNew->imageChanges;
std::swap(pImpl, pNew); // Swap (nothrow)
// Old data automatically deleted
}
```text
### Item 30: Understand the Ins and Outs of Inlining
**Principle:** Limit inlining to small, frequently-called functions.
**Inline is a request, not command:**
```cpp
// Implicitly inline
class Person {
public:
int age() const { return theAge; } // Implicitly inline
private:
int theAge;
};
// Explicitly inline
inline void f() { ... } // Inline request
template<typename T>
void g(T x) { ... } // Templates usually in headers, often inlined
```text
**Problems with inline:**
1. **Code bloat** - inline replaces each call with function body
2. **Paging issues** - larger code can reduce cache hit rate
3. **Can't debug** - can't set breakpoint in inline function
4. **Binary compatibility** - changing inline function requires recompiling all clients
**When NOT to inline:**
- Functions with loops or recursion
- Virtual functions (usually can't be inlined)
- Functions called through pointers
- Constructors/destructors (often do more than you think)
```cpp
// Looks simple but probably shouldn't be inline
class Base {
public:
Base() { ... } // Calls base constructors, initializes members
~Base() { ... } // Calls destructors, exception handling
};
```text
### Item 31: Minimize Compilation Dependencies Between Files
**Principle:** Depend on declarations, not definitions. Use pimpl idiom and interface classes.
**Problem - include files increase dependencies:**
```cpp
// Person.h
#include <string>
#include "date.h"
#include "address.h"
class Person {
public:
Person(const std::string& name, const Date& birthday, const Address& addr);
std::string name() const;
// ...
private:
std::string theName; // Requires string definition
Date theBirthDate; // Requires Date definition
Address theAddress; // Requires Address definition
};
```text
**Solution 1: Pimpl (Pointer to Implementation) Idiom:**
```cpp
// Person.h
#include <memory> // For shared_ptr
class PersonImpl; // Forward declaration
class Date; // Forward declaration
class Address; // Forward declaration
class Person {
public:
Person(const std::string& name, const Date& birthday, const Address& addr);
std::string name() const;
// ...
private:
std::shared_ptr<PersonImpl> pImpl; // Pointer to implementation
};
// Person.cpp
#include "Person.h"
#include "PersonImpl.h" // Implementation details
Person::Person(const std::string& name, const Date& birthday, const Address& addr)
: pImpl(new PersonImpl(name, birthday, addr))
{ }
std::string Person::name() const {
return pImpl->name();
}
```text
**Solution 2: Interface Classes (Abstract Base Classes):**
```cpp
// Person.h
class Person {
public:
virtual ~Person();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
static std::shared_ptr<Person> create(const std::string& name,
const Date& birthday,
const Address& addr);
};
// RealPerson.cpp
class RealPerson: public Person {
public:
RealPerson(const std::string& name, const Date& birthday, const Address& addr)
: theName(name), theBirthDate(birthday), theAddress(addr)
{ }
virtual ~RealPerson() { }
std::string name() const { return theName; }
// ...
private:
std::string theName;
Date theBirthDate;
Address theAddress;
};
std::shared_ptr<Person> Person::create(const std::string& name,
const Date& birthday,
const Address& addr) {
return std::shared_ptr<Person>(new RealPerson(name, birthday, addr));
}
// Client code
std::shared_ptr<Person> pp(Person::create(name, dateOfBirth, address));
std::cout << pp->name();
```text