본문으로 건너뛰기

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/0 can 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:

  • short values are promoted to int in 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:

  1. Single-argument constructors.
  2. 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