본문으로 건너뛰기

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