Designing a Function
This chapter explains how to design individual function interfaces that provide safe, readable, efficient, and maintainable access to an abstraction.
9.1 Function Interface Specification
Designing a C++ function interface requires answering fourteen questions.
1. Operator or non-operator?
2. Free or member operator?
3. Virtual or non-virtual?
4. Pure or non-pure virtual?
5. Static or non-static member?
6. const or non-const member?
7. Public, protected, or private?
8. Return by value, reference, pointer, or argument?
9. Return const or non-const?
10. Argument optional or required?
11. Pass argument by value, reference, or pointer?
12. Pass argument as const or non-const?
13. Friend or non-friend?
14. Inline or non-inline?
These decisions interact with one another and should be made according to the semantics of the abstraction rather than personal style.
9.1.1 Operator or Non-Operator Function
Operator Overloading:
Using C++ operator notation to represent an operation on a user-defined type.
a + b
a == b
++a
The main reason to use operator notation is readability.
Principle:
Readability, more than ease of use, should be the primary reason for employing operator overloading.
Natural Semantics
Guideline:
The semantics of an overloaded operator should be natural, obvious, and intuitive to clients.
Good:
setA + setB;
pointA == pointB;
Bad:
~string; // unexpectedly means "reverse string"
Do not assign surprising meanings to familiar operators.
Match Fundamental-Type Syntax
Guideline:
The syntactic properties of overloaded operators for user-defined types should mirror those already defined for fundamental types.
Principle:
Patterning user-defined operators after predefined C++ operators avoids surprises and makes their use predictable.
Return Categories
Operators that modify an object generally return a modifiable reference.
T& operator=(const T&);
T& operator+=(const T&);
T& operator++(); // prefix
Operators that create a new value generally return by value.
T operator+(const T&, const T&);
T operator-(const T&, const T&);
Postfix increment and decrement return the old value by value.
T operator++(int);
T operator--(int);
Equality and relational operators return a Boolean-like value.
int operator==(const T&, const T&);
int operator< (const T&, const T&);
Operator Notation Test
Use operator notation only when the user-defined operation behaves like the corresponding fundamental operation.
If the semantics differ substantially, prefer an ordinary named function.
Natural Fundamental-Like Semantics
↓
Operator
Different / Surprising Semantics
↓
Named Function
9.1.2 Free or Member Operator
The main issue is whether implicit user-defined conversion of the left operand should be allowed.
Member Operator
Use a member operator when:
- The operator modifies the left operand.
- Implicit conversion of the left operand should be prohibited.
- The language requires the operator to be a member.
Typical examples:
operator=
operator+=
operator-=
operator*=
operator++
operator--
Free Operator
Use a free operator when:
- The operation is symmetric.
- Implicit conversion should be available to both operands.
- Neither operand is modified.
Typical examples:
operator==
operator!=
operator<
operator<=
operator>
operator>=
operator+
operator-
operator*
operator/
Member
└── Left operand is special
Free
└── Operands should be symmetric
Principle:
The C++ language itself provides an objective model for designing user-defined operators.
Symmetric Conversion
A symmetric binary operator should generally remain free.
String operator+(const String& left,
const String& right);
int operator==(const String& left,
const String& right);
This allows implicit conversions on either side.
Required Member Operators
The book lists operators that C++ requires to be members:
= Assignment
[] Subscript
-> Member access
() Function call
(T) Conversion operator
new Class-specific allocation
delete Class-specific deallocation
Principle:
Inconsistency among overloaded operators is obvious, annoying, and costly to clients.
9.1.3 Virtual or Non-Virtual Function
Virtual Function:
A member function whose implementation can be selected dynamically according to the actual object type.
Use a virtual function when behavior must vary among derived types.
class Shape
{
public:
virtual void draw() const = 0;
};
Principle:
Virtual functions implement variation in behavior; data members implement variation in value.
Different Behavior
↓
Virtual Function / Polymorphism
Different Value
↓
Data Member
Do not introduce inheritance merely to represent different stored values.
Free Operators with Polymorphism
Polymorphic behavior does not require symmetric operators to become members.
A common pattern is:
class Shape
{
public:
virtual int compare(const Shape& other) const = 0;
};
inline int operator==(const Shape& a, const Shape& b)
{
return a.compare(b) == 0;
}
Virtual Primitive Member
↓
Free Symmetric Operators
Principle:
Syntactic symmetry need not be sacrificed to obtain polymorphic behavior.
Hide, Overload, Override, Redefine
Hide:
A member function hides a function with the same name in a base class or enclosing scope.
Overload:
Two or more different functions share the same name in the same scope.
Override:
A derived-class member replaces the behavior of an identical virtual function declared in a base class.
Redefine:
A default function definition is replaced so that the original definition is no longer accessible.
Guideline:
Avoid hiding a base-class function in a derived class.
In particular, do not redefine a non-virtual base-class function in a derived class.
9.1.4 Pure or Non-Pure Virtual Member Function
Pure Virtual Function:
A virtual member function that requires a concrete derived class to provide behavior.
virtual void draw() = 0;
Use a pure virtual function when failing to override the behavior would likely be an error.
Protocol classes should normally declare their behavioral functions pure virtual.
Behavior Must Be Supplied
↓
Pure Virtual
Useful Default Behavior Exists
↓
Non-Pure Virtual
A pure virtual function may still have a definition that a derived implementation explicitly invokes.
9.1.5 Static or Non-Static Member Function
Static Member Function:
A member function that does not operate on one particular object instance.
class Widget
{
static int s_count;
public:
static int instanceCount();
};
Use static functions for:
- Operations independent of object state.
- Utility functionality grouped under a type.
- Non-primitive behavior escalated from lower-level objects.
- Operations needing argument symmetry.
Principle:
Static member functions are commonly used to implement non-primitive functionality in a separate utility class.
struct PointUtil
{
static int compareMagnitude(const Point& a,
const Point& b);
};
9.1.6 const Member or Non-const Member Function
A member function should be declared const when doing so accurately represents the logical behavior of the abstraction.
Physical Constness
Physical Constness:
Whether the bits stored directly inside an object are modified.
This is what the C++ type system directly enforces.
Logical Constness
Logical Constness:
Whether the object's observable logical value changes from the client's point of view.
A function may be physically capable of modifying internal storage while still being logically const if the modification is not observable.
Const-Correct Object
Const-Correct Object:
An object for which a function receiving only a const reference cannot obtain writable access to that same object or any part of it without explicitly casting away const.
Guideline:
Every object in a system should be const-correct.
Const-Correct System
Const-Correct System:
A system in which const references cannot be followed through the object network to obtain writable references to the same objects without an explicit cast.
const Object&
↓
Only const access paths
↓
Const-Correct
A const function returning a non-const pointer or reference can destroy const-correctness.
Bad:
Node* child() const;
Better:
Node* child();
const Node* child() const;
Principle:
Returning a non-const object from a const member function can rupture system const-correctness.
Guideline:
Think twice before casting away const.
9.1.7 Public, Protected, or Private Member Function
Public
Use public for operations intended for general clients.
General Client
↓
public
Protected
Use protected for operations intended specifically for derived-class authors.
Derived-Class Author
↓
protected
Protected interfaces expose implementation details to every client that sees the class definition and should therefore be used carefully.
Private
Use private for implementation details.
Useful cases include:
- Non-virtual helpers used only inside the class.
- Helpers factored out of small public inline functions.
- Private virtual functions whose behavior derived classes must provide but need not call directly.
Principle:
Member functions that are not public expose general users to uninsulated implementation details.
Principle:
All virtual and protected functions are intended for consideration by derived-class authors.
9.1.8 Return by Value, Reference, Pointer, or Argument
The return mechanism should express the semantics of the operation.
Return by Value
Use when:
- No preexisting object is appropriate to reference.
- A new value is produced.
- Total encapsulation is important.
Point center() const;
Return by Reference
Use when:
- There is always a valid existing object to return.
- The returned object's lifetime is managed elsewhere.
const Point& origin() const;
Return by Pointer
Use when:
- There may be no object to return.
nullptr/0can naturally represent failure.- Returning access to an existing polymorphic object.
const Node* lookup(const char* name) const;
Return by Argument
Use when:
- More than one item must be returned.
- Both status and value must be returned.
- A heavyweight value should be returned efficiently.
- A dynamically allocated polymorphic result should be loaded into a managing handle.
int lookup(Node* result, const char* name);
Value
└── New value
Reference
└── Existing object always exists
Pointer
└── Existing object may not exist
Argument
└── Multiple / heavy / status + value
Status Values
Guideline:
For functions returning an error status, integral value 0 should mean success.
0
└── Success
Non-Zero
└── Failure
Principle:
Often there is one way for an operation to succeed and several ways for it to fail.
Boolean Questions
Guideline:
Functions answering yes-or-no questions should be named accordingly and return 0 for no and 1 for yes.
Examples:
isValid()
hasProtocol()
areParallel()
Dynamically Allocated Results
Principle:
Loading a newly allocated object into a modifiable handle argument is less prone to memory leaks than returning the object through a raw non-const pointer.
9.1.9 Return const or Non-const
Guideline:
Avoid declaring values returned by value as const.
Bad:
const int size();
Preferred:
int size();
A returned value is already a temporary copy, so const usually adds unnecessary restrictions.
For pointers and references, returning const can preserve const-correctness and implementation flexibility.
const Node* child() const;
const Point& origin() const;
9.1.10 Argument Optional or Required
Default Argument:
A value supplied automatically when the caller omits an argument.
Point(int x = 0, int y = 0);
Principle:
Default arguments can be an effective alternative to function overloading, especially when insulation is not important.
Advantages:
- Compact interface.
- Self-documenting defaults.
- One implementation body.
- Convenient extension of existing functions.
Disadvantages:
- Default values reside in headers.
- Changing defaults forces clients to recompile.
- Certain defaults can enable unwanted implicit conversions.
User-Defined Default Values
Guideline:
Avoid default arguments that require construction of an unnamed temporary object.
Cheap Fundamental Default
↓
Often Acceptable
User-Defined Temporary Default
↓
Usually Avoid
For widely used insulating interfaces, required arguments are generally safer.
9.1.11 Pass Argument by Value, Reference, or Pointer
Fundamental Types
Pass fundamental and enumerated types by value.
void setX(int x);
void setMode(Mode mode);
User-Defined Types
Minor Design Rule:
Never pass a user-defined type to a function by value.
Instead, pass read-only user-defined objects by const reference.
void process(const Widget& widget);
This avoids unnecessary construction and copying.
Modifiable Arguments
The book recommends using a non-const pointer to make modification obvious at the call site.
void update(Widget* widget);
Client:
update(&widget);
The & visually advertises that the object may be modified.
Guideline:
Be consistent about returning values through arguments; avoid mixing conventions such as widespread non-const reference output parameters.
Optional Output
A pointer can naturally indicate that an output is optional.
void calculate(Result* optionalResult);
Retaining an Argument Address
Guideline:
If a function stores an argument's address beyond the function call, pass the argument by pointer rather than reference.
void add(const Object* object);
This makes the lvalue/lifetime requirement visible and prevents an implicit temporary from silently supplying the stored address.
Deleting Arguments
Minor Design Rule:
Never attempt to delete an object passed by reference.
If ownership transfer or deletion occurs, use a pointer.
void destroy(Object* object);
9.1.12 Pass Argument as const or Non-const
Guideline:
A reference or pointer parameter should be const whenever the function neither modifies the object nor stores a writable address to it.
void print(const Widget& widget);
void inspect(const Widget* widget);
Guideline:
Avoid declaring parameters passed by value as const.
Bad:
void f(const int value);
Preferred:
void f(int value);
Whether the local copy is modified is an implementation detail and does not belong in the interface.
Parameter Ordering
Guideline:
Consider placing modifiable parameters before value, const-reference, and const-pointer parameters.
void calculate(Result* result,
const Input& input,
int mode);
This creates a consistent location for output or modifiable arguments.
9.1.13 Friend or Non-Friend Function
Friend Function:
A non-member function granted access to a class's private and protected members.
Friendship increases maintenance coupling.
Principle:
Avoiding unnecessary friendship, even within one component, can improve maintainability.
Before declaring a free operator as a friend, look for a public primitive operation that can implement it.
Example:
class Value
{
public:
int compare(const Value& other) const;
};
inline int operator==(const Value& a, const Value& b)
{
return a.compare(b) == 0;
}
No friendship is required.
Guideline:
Avoid granting friendship to individual functions.
9.1.14 Inline or Non-Inline Function
Inline Function:
A function whose definition is placed where the compiler may substitute the function body directly at call sites.
Inlining affects:
- Runtime overhead.
- Executable size.
- Compile-time coupling.
- Insulation.
When to Inline
Reasonable candidates include tiny accessor and manipulator functions.
int size() const
{
return d_size;
}
When Not to Inline
Guideline:
Avoid declaring a function inline when its generated body is larger than the equivalent non-inline function call.
Guideline:
Avoid declaring a function inline when the compiler will not actually inline it.
Large inline functions can:
- Increase executable size.
- Duplicate code across translation units.
- Reduce insulation.
- Potentially hurt runtime performance.
Tiny + Frequently Used
↓
Possible Inline
Large / Widely Called / Insulation Important
↓
Non-Inline
9.2 Fundamental Types Used in the Interface
The book argues for minimizing the variety of fundamental numeric types exposed in function interfaces.
9.2.1 Using short in the Interface
Guideline:
Avoid using short in the interface; use int instead.
Reasons include:
shortvalues are promoted tointin expressions.- Overflow can occur before the function can detect it.
- Representation constraints leak into the interface.
- Overload resolution can become ambiguous.
- Template instantiation becomes more awkward.
Internal Storage
└── May Use short
Public Interface
└── Prefer int
9.2.2 Using unsigned in the Interface
Guideline:
Avoid using unsigned in the interface; use int instead.
Mixing signed and unsigned values can silently reinterpret negative values as large positive values.
unsigned int value = 3;
// Surprising signed/unsigned interaction
value > -1;
Problems include:
- Negative values are not prevented.
- Runtime checking becomes harder.
- Type conversions can cause subtle errors.
- Overload resolution may become ambiguous.
- Template use becomes less convenient.
- Implementation constraints leak into the interface.
The book prefers documenting a non-negative requirement rather than using unsigned to express it.
9.2.3 Using long in the Interface
Guideline:
Avoid using long in the interface; use int or a dedicated user-defined large-integer type.
long does not portably guarantee a substantially larger range than int.
Problems include:
- Platform-dependent width.
- Conversion warnings.
- Potential information loss.
- Overload ambiguity.
- Template issues.
Normal Integer
└── int
Needs Guaranteed Larger Capacity
└── User-Defined Large Integer Type
9.2.4 Using float, double, and long double
Guideline:
Consider using double exclusively for floating-point interface values unless there is a compelling reason to use another floating type.
Reasons include:
- Common library conventions.
- Hardware efficiency.
- Reduced interface complexity.
- Fewer overload and conversion problems.
Principle:
In most practical interfaces, int and double are sufficient fundamental types for integer and floating-point values.
Integer Interface
└── int
Floating-Point Interface
└── double
9.3 Special-Case Functions
This section covers function types that require special design consideration.
9.3.1 Conversion Operators
C++ supports two forms of implicit user-defined conversion:
- Single-argument constructors.
- Conversion operators.
Single-Argument Constructor
class String
{
public:
String(const char* value);
};
This allows implicit conversion:
String s = "hello";
Principle:
Constructors enabling implicit conversion, especially from widely used fundamental types such as int, erode type safety.
A constructor such as:
NodeId(int index);
may cause unrelated integer expressions to convert silently to NodeId.
Conversion Operator
operator const char*() const;
This also enables implicit conversion.
Guideline:
Consider avoiding conversion operators, especially conversions to fundamental integral types; provide an explicit conversion function instead.
Instead of:
operator int() const;
prefer:
int value() const;
Implicit Conversion
├── Convenient
└── Less Type-Safe
Explicit Conversion
├── More Verbose
└── Safer + Clearer
Use implicit conversion only when the semantics are tightly coupled and unlikely to surprise clients.
9.3.2 Compiler-Generated Value Semantics
C++ may automatically generate:
- Copy constructor.
- Assignment operator.
Before accepting the generated behavior, decide whether the class should have value semantics at all.
No Value Semantics
If copying or assignment is inappropriate, explicitly prevent it.
class Object
{
Object(const Object&);
Object& operator=(const Object&);
};
Value Semantics Required
If copying and assignment make sense, determine whether memberwise compiler-generated behavior is correct.
If not, provide explicit definitions.
Guideline:
Explicitly declare the copy constructor and assignment operator for any class defined in a header file, even when the default implementation would be adequate.
The purpose is to make the class author's intended value semantics explicit.
9.3.3 The Destructor
Destructor:
A special member function responsible for destroying an object and releasing resources managed by it.
~Object();
Classes with Virtual Functions
Minor Design Rule:
In every class that declares a virtual function, or derives from one that does, explicitly declare the destructor as the first virtual function and define it out of line.
class Shape
{
public:
virtual ~Shape();
virtual void draw() const = 0;
};
Implementation:
Shape::~Shape()
{
}
Reasons include:
- Correct destruction through base pointers.
- Derived classes may manage additional resources.
- A non-inline virtual function gives some implementations a unique translation unit for virtual-table-related definitions.
Classes without Virtual Functions
Guideline:
If a class otherwise has no virtual functions, explicitly declare a non-virtual destructor and define it inline or out of line as appropriate.
Do not make the destructor virtual merely as a precaution when polymorphic destruction is not part of the design.
Protocol Class
For a protocol class:
- Destructor is virtual.
- Destructor is defined out of line.
- Destructor implementation is empty.
Protocol::~Protocol()
{
}
9.4 Function Design Summary
1. Operator or Non-Operator
Operator
└── Natural syntax improves readability
Non-Operator
└── Operation does not mirror fundamental syntax
2. Free or Member Operator
Free
├── Symmetric
└── Allow conversion of left operand
Member
├── Modifies left operand
├── Suppress left conversion
└── Required by language
3. Virtual or Non-Virtual
Virtual
└── Behavior varies by derived type
Non-Virtual
├── Symmetric free operator
├── Unary operator needing argument conversion
└── Variation is merely stored value
4. Pure or Non-Pure Virtual
Pure
└── Derived class must supply behavior
Non-Pure
└── Useful default behavior exists
5. Static or Non-Static
Static
├── No specific instance needed
├── Utility / non-primitive operation
└── Argument symmetry desired
Non-Static
└── Depends on one object's state
6. const or Non-const
const
└── No logical modification
Non-const
└── Logical state changes
7. Access Level
public
└── General clients
protected
└── Derived-class authors
private
└── Implementation detail
8. Return Mechanism
Value
└── New value
Reference
└── Existing value always exists
Pointer
└── Existing value may be absent
Argument
├── Heavy result
├── Multiple results
└── Status + result
9. Return const
By Value
└── Usually non-const
By Pointer / Reference
└── const when required for const-correctness
10. Optional Arguments
Default Argument
└── One algorithm + insulation not critical
Required Argument
├── Widely used interface
├── User-defined default would be expensive
└── Insulation matters
11. Argument Passing
Fundamental / Enum
└── By Value
Read-Only User Type
└── const Reference
Modified / Stored / Deleted / Optional
└── Pointer
12. Argument Constness
Reference / Pointer Not Modified
└── const
Passed by Value
└── Do not expose const in interface
13. Friendship
Prefer
└── Non-Friend
Friend
└── Only when genuinely necessary
14. Inlining
Inline
├── Tiny
├── Performance relevant
└── Small call-site count
Non-Inline
├── Insulation important
├── Function too large
└── Compiler will not inline
9.5 Key Concepts
Function Design
↓
Safe + Readable + Efficient Interface
Operator Design
├── Natural Semantics
├── Match Fundamental Syntax
├── Symmetric → Free
└── Modifying → Member
Polymorphism
├── Virtual = Behavior Variation
├── Pure Virtual = Required Override
└── Avoid Hiding
Const-Correctness
├── Object
└── Whole System
Return Design
├── Value
├── Reference
├── Pointer
└── Argument
Argument Design
├── Fundamental → Value
├── User Type Read-Only → const&
└── Modified / Stored / Deleted → Pointer
Interface Numeric Types
├── Integer → int
└── Floating Point → double
Implicit Conversion
↓
Reduced Type Safety
Value Semantics
├── Copy Constructor
└── Assignment Operator
Destructor
├── Virtual Hierarchy → Virtual + Out-of-Line
└── Non-Virtual Class → Explicit Non-Virtual Destructor