본문으로 건너뛰기

Tools for Large Programs

Large programs need mechanisms for:

  • reporting errors across subsystem boundaries
  • avoiding name collisions between independent libraries
  • modeling complex inheritance relationships

This chapter focuses on:

exception handling
namespaces
multiple inheritance
virtual inheritance

18.1 Exception Handling

Exception Handling: A mechanism that separates detection of a run-time problem from the code that handles it.

The core language constructs are:

throw
try
catch

18.1.1 Throwing an Exception

throw Expression: Raises an exception and transfers control to a matching handler.

if (value < 0)
{
throw std::runtime_error(
"negative value"
);
}

The type of the thrown expression determines which handlers can match it.


Stack Unwinding

When an exception is thrown, the program searches outward through active function calls for a matching catch.

This process is called stack unwinding.

Conceptually:

function C throws

no handler in C

leave C

no handler in B

leave B

matching handler in A

Local objects in scopes that are exited during unwinding are destroyed automatically.


Automatic Cleanup during Unwinding

void function()
{
std::string text =
"data";

throw std::runtime_error(
"failure"
);
}

When the exception leaves function(), text is destroyed normally.

This is one reason resource ownership should be tied to object lifetime.


Uncaught Exceptions

If no matching handler is found, the program calls:

std::terminate();

and execution stops.


Exception Objects

The expression in a throw is used to initialize a separate exception object.

std::runtime_error error(
"failure"
);

throw error;

The exception object exists until exception handling is complete.

Avoid throwing pointers to local objects because those local objects may be destroyed during stack unwinding.


Destructors and Exceptions

Destructors are often executed during stack unwinding.

A destructor should normally not allow an exception to escape while another exception is already being handled.

If a second exception escapes during unwinding, program termination can result.


18.1.2 Catching an Exception

A handler has the form:

catch (const Type& error)
{
// handle error
}

The catch parameter type determines which exceptions the handler can accept.


Catch by Reference

For class-type exceptions, catch by reference is usually preferred.

catch (
const std::runtime_error& error)
{
std::cerr
<< error.what()
<< '\n';
}

This:

  • avoids copying the exception object
  • preserves polymorphic behavior
  • avoids slicing derived exception objects

Handler Order

Catch clauses are tested in source order.

The first matching handler is selected.

Therefore handlers for derived exception types must appear before handlers for their base types.

try
{
operation();
}
catch (
const std::overflow_error& error)
{
// more specific
}
catch (
const std::runtime_error& error)
{
// more general
}

Reversing this order could make the more specific handler unreachable.


Matching Rules

Exception matching allows fewer conversions than ordinary function calls.

Important allowed matches include:

  • exact type
  • nonconst to const
  • derived exception to base handler
  • array-to-pointer conversion
  • function-to-pointer conversion

Ordinary arithmetic and user-defined conversions are not used to match a catch.


Rethrowing an Exception

A handler may perform partial work and then rethrow the current exception.

try
{
operation();
}
catch (...)
{
cleanup();

throw;
}

A bare:

throw;

rethrows the current exception object.


Catch-All Handler

catch (...)
{
// handles any exception type
}

A catch-all handler matches every exception.

If used with other handlers, it must appear last.


18.1.3 Function try Blocks and Constructors

A normal try block inside a constructor body cannot catch an exception thrown while initializing a base class or data member.

Those initializations occur before the constructor body begins.

Use a function try block.

Widget::Widget(
int value)
try
: data(value)
{
}
catch (
const std::exception& error)
{
handle(error);
}

The try keyword appears before the constructor initializer list.


Function Try Block Scope

A constructor function try block can catch exceptions thrown from:

  • base-class initialization
  • data-member initialization
  • constructor body

It cannot catch exceptions thrown while evaluating the constructor's arguments before the constructor itself begins execution.


18.1.4 The noexcept Exception Specification

noexcept: Declares that a function promises not to let exceptions escape.

void reset() noexcept;

A function without such a specification may throw.

void allocate();

noexcept Must Match across Declarations

If a function is declared noexcept, the specification must appear consistently on its declarations and definition.

void clear() noexcept;

void clear() noexcept
{
}

Throwing from a noexcept Function

The compiler does not generally prove that a noexcept function cannot throw.

This is legal to compile:

void function() noexcept
{
throw std::runtime_error(
"error"
);
}

but if the exception escapes the function, the program calls:

std::terminate();

Conditional noexcept

noexcept can contain a constant expression.

void function()
noexcept(condition);

If the expression is true, the function is nonthrowing.


The noexcept Operator

The noexcept operator tests whether an expression is declared nonthrowing.

bool result =
noexcept(function());

The expression itself is not executed.

The result is a compile-time bool.


Why noexcept Matters

A nonthrowing specification:

  • documents part of the function interface
  • lets callers know exceptions will not escape
  • can enable implementation optimizations
  • is especially important for operations such as move constructors and swap

18.1.5 Exception Class Hierarchies

The standard library organizes many exception types into inheritance hierarchies.

A simplified structure is:

exception
├─ bad_alloc
├─ bad_cast
├─ runtime_error
│ ├─ overflow_error
│ ├─ underflow_error
│ └─ range_error
└─ logic_error
├─ domain_error
├─ invalid_argument
├─ length_error
└─ out_of_range

exception

The root type is:

std::exception

It defines the virtual member:

what()

which returns a C-style character string describing the problem.


Standard Exception Groups

logic_error: Represents problems that could often be prevented by correct program logic.

runtime_error: Represents problems that can arise only while the program is running.

Example:

throw std::out_of_range(
"index out of range"
);

Custom Exception Hierarchies

Application-specific exception types can derive from standard exception classes.

class FileError
: public std::runtime_error
{
public:
explicit FileError(
const std::string& message)
: std::runtime_error(
message)
{
}
};

Usage:

throw FileError(
"cannot open file"
);

Catch through the custom type:

catch (
const FileError& error)
{
}

or through its base:

catch (
const std::runtime_error& error)
{
}

18.2 Namespaces

Namespace: A named scope used to organize declarations and prevent name collisions.

Large programs often combine code written by independent libraries.

Namespaces allow identical names to coexist safely.

namespace graphics
{
class Window;
}

namespace database
{
class Window;
}

These are distinct types:

graphics::Window
database::Window

18.2.1 Namespace Definitions

Basic syntax:

namespace project
{
int value;

void process();
}

Access a namespace member using the scope operator.

project::value = 10;

project::process();

Namespaces Can Be Reopened

A namespace definition may be split across files or locations.

namespace project
{
class Engine;
}

Later:

namespace project
{
void run();
}

Both declarations belong to the same namespace.

This is essential for large libraries.


Namespace Members Defined outside the Namespace Body

A member declared in a namespace can be defined later with a qualified name.

namespace project
{
void run();
}

void project::run()
{
}

The definition must appear in a scope that encloses the namespace.


Nested Namespaces

C++11 uses ordinary nesting syntax.

namespace project
{
namespace io
{
void read();
}
}

Access:

project::io::read();

Global Namespace

Names declared outside any named namespace belong to the global namespace.

Access can be explicitly qualified with:

::name

Example:

int value = 10;

void function()
{
int value = 20;

std::cout
<< ::value;
}

Unnamed Namespace

An unnamed namespace gives its members internal linkage within the translation unit.

namespace
{
int local_value = 0;

void helper()
{
}
}

The names can be used directly in that translation unit.

Unnamed namespaces are the C++ mechanism for file-local implementation names.


Unnamed Namespace and File static

Older C-style code often used:

static int value;

at file scope to make a name local to one source file.

In C++, unnamed namespaces are generally preferred for this purpose.


Inline Namespace

C++11 also provides inline namespaces.

namespace library
{
inline namespace v2
{
void function();
}
}

Members of the inline namespace can be used as though they were direct members of the enclosing namespace.

library::function();

Inline namespaces are useful for versioned library interfaces.


18.2.2 Using Namespace Members

There are three common ways to shorten namespace-qualified names:

  1. namespace alias
  2. using declaration
  3. using directive

Namespace Alias

A namespace alias gives a shorter name to an existing namespace.

namespace long_library_name
{
void run();
}

namespace lib =
long_library_name;

Usage:

lib::run();

Aliases are especially useful for nested or long namespace names.


using Declaration

A using declaration introduces one name.

using std::string;

Now:

string text;

can be used in that scope.

A using declaration provides precise control over which names become available.


Scope of a using Declaration

A name introduced by a using declaration follows normal scope rules.

void function()
{
using std::string;

string value;
}

Outside the function, the using declaration has no effect.


using namespace Directive

A using directive makes all names from a namespace available for unqualified lookup.

using namespace std;

This is less precise than a using declaration.


Avoid Broad Using Directives in Headers

A header containing:

using namespace std;

can unexpectedly introduce many names into every file that includes it.

Avoid broad using directives in headers and large shared scopes.

Prefer:

std::string
```

or targeted using declarations.

---

### Difference between Declaration and Directive

~~~text
using declaration
imports one name

using directive
makes all namespace names available
```

A using declaration behaves more like an ordinary declaration in the current scope.

A using directive affects name lookup more indirectly.

---

### 18.2.3 Classes, Namespaces, and Scope

Namespaces interact with class scope, friendship, and function lookup.

---

### Friend Declarations Can Introduce Namespace Members

Suppose:

~~~cpp
namespace tools
{
class Widget
{
friend void inspect(
const Widget&);
};
}

The friend declaration makes inspect a member of namespace tools.

However, a separate namespace-scope declaration may still be needed for ordinary qualified lookup before the function is otherwise declared.


Argument-Dependent Lookup

When an unqualified function call has an argument of class type, the compiler also searches namespaces associated with the argument's type.

This is called argument-dependent lookup (ADL).

namespace tools
{
class Widget
{
};

void inspect(
const Widget&)
{
}
}

Then:

tools::Widget object;

inspect(object);

can find:

tools::inspect

even though inspect was not explicitly qualified.


Why swap Uses ADL

A common generic pattern is:

using std::swap;

swap(a, b);

This allows:

  • a type-specific swap found through ADL
  • std::swap as a fallback

That is why directly writing:

std::swap(a, b);

can be less flexible for user-defined types.


18.2.4 Overloading and Namespaces

Namespaces affect function overload resolution by contributing candidate functions.


using Declarations and Overloads

A using declaration names the function name, not one specific overload.

Correct:

using library::print;

Invalid:

// using library::print(int);

All overloads of print visible through that namespace name are introduced.


ADL Adds Candidates

For a call:

display(object);

if object has a class type defined in namespace NS, functions named display in NS may be added to the candidate set.

This applies even when those functions are not otherwise visible at the call site.


Using Directives and Overload Sets

Functions made visible through using directives can participate in the same overload set as functions from other scopes or namespaces.

namespace A
{
void print(int);
}

namespace B
{
void print(double);
}

using namespace A;
using namespace B;

Then overload resolution may choose between both versions.


Name Collisions

A using directive does not necessarily cause an immediate error when two namespaces contain the same name.

An error arises when an unqualified use is ambiguous.

Prefer explicit qualification when library boundaries make names unclear.


18.3 Multiple and Virtual Inheritance

Multiple Inheritance: Deriving one class directly from more than one base class.

class Panda
: public Bear,
public Endangered
{
};

A multiply derived class contains base subobjects corresponding to each direct base.

Multiple inheritance is powerful but increases the possibility of:

  • ambiguous names
  • ambiguous conversions
  • duplicated base subobjects

18.3.1 Multiple Inheritance

A class lists several direct bases in its derivation list.

class Derived
: public Base1,
public Base2
{
};

Each base has its own access specifier.


Base Initialization Order

Base classes are initialized in the order in which they appear in the derivation list, not the order written in the constructor initializer list.

class Derived
: public Base1,
public Base2
{
public:
Derived()
: Base2(),
Base1()
{
}
};

The actual initialization order is:

Base1
Base2
Derived members
Derived constructor body

Copy and Move Control

Synthesized copy-control members process every base-class subobject and then the derived members.

A user-defined copy or move constructor should explicitly initialize all required base classes.


Destruction Order

Destruction occurs in reverse construction order.

Derived destructor body
derived members
Base2
Base1

18.3.2 Conversions and Multiple Base Classes

A multiply derived object may be converted to a pointer or reference to any accessible direct or indirect base.

Panda panda;

Bear* bear =
&panda;

Endangered* endangered =
&panda;

Both conversions are valid when the bases are accessible.


Ambiguous Base Conversion

If the same base type appears through multiple inheritance paths, conversion to that base may be ambiguous.

Conceptually:

Base
/ \
Derived1 Derived2
\ /
Final

Without virtual inheritance, Final contains two separate Base subobjects.

Then:

Base* pointer =
&final_object;

is ambiguous because the compiler cannot know which Base subobject is intended.


Pointer/Reference Dynamic Binding Still Works

Once a base pointer or reference unambiguously refers to the proper base subobject, virtual function calls still use the dynamic type normally.

Multiple inheritance does not disable virtual dispatch.


18.3.3 Class Scope under Multiple Inheritance

Name lookup searches all relevant base-class branches.

If the same unqualified name is found through more than one base path, the use may be ambiguous.

struct Base1
{
void print();
};

struct Base2
{
void print();
};

struct Derived
: Base1,
Base2
{
};

Then:

Derived object;

// Error:
// object.print();

Both inherited names are visible.


Resolve with Qualification

object.Base1::print();

or:

object.Base2::print();

Prefer Resolving Ambiguity in the Derived Class

A better interface can provide one derived-class function.

struct Derived
: Base1,
Base2
{
void print()
{
Base1::print();
}
};

Client code can then call:

object.print();

without knowing inheritance details.


Name Lookup Precedes Type Checking

An ambiguity can occur even when only one candidate would otherwise be a valid overload after argument checking.

Name lookup is resolved before ordinary overload matching.

Therefore inheritance ambiguities should be resolved explicitly rather than expecting overload resolution to fix them.


18.3.4 Virtual Inheritance

Virtual Inheritance: Makes multiple inheritance paths share one common base-class subobject.

Without virtual inheritance:

Base
/ \
D1 D2
\ /
Final

Final contains:
Base through D1
Base through D2

With virtual inheritance:

Base
/ \
virtual virtual
D1 D2
\ /
Final

Final contains:
one shared Base

Declaring a Virtual Base

class D1
: virtual public Base
{
};

class D2
: virtual public Base
{
};

class Final
: public D1,
public D2
{
};

The keyword:

virtual

means the Base subobject is shared in the most-derived object.


Why Virtual Inheritance Exists

The standard IO hierarchy is a classic example.

Conceptually:

basic_ios
/ \
istream ostream
\ /
iostream

iostream needs one shared stream state rather than two independent copies of basic_ios.

Virtual inheritance solves this duplicated-base problem.


Virtual Base Is Shared Only in the Complete Object

D1 and D2 may each be instantiated independently.

When used as separate complete objects, each still contains its own virtual base.

When both appear inside a more-derived object, they share a single virtual-base subobject.


Scope with Virtual Bases

If a name exists only in the shared virtual base, there is no ambiguity because the most-derived object contains only one virtual-base subobject.

However, if both intermediate classes redefine the same name, the final derived class may still face an ambiguity.

Virtual inheritance removes duplicate base subobjects; it does not eliminate every possible name conflict.


18.3.5 Constructors and Virtual Inheritance

A virtual base is initialized by the most-derived constructor.

Suppose:

ZooAnimal
/ \
Bear Raccoon
\ /
Panda

where Bear and Raccoon inherit ZooAnimal virtually.

When constructing a Panda, the Panda constructor controls initialization of the shared ZooAnimal base.


Most-Derived Class

Most-Derived Class: The actual complete class being constructed.

For:

Panda object;

the most-derived class is:

Panda

For:

Bear object;

the most-derived class is:

Bear

Therefore a class that virtually inherits a base must still provide a sensible virtual-base initialization path because it may itself be instantiated as the complete object.


Virtual-Base Initialization

Example:

Panda::Panda(
const std::string& name)
: ZooAnimal(name),
Bear(name),
Raccoon(name)
{
}

ZooAnimal is initialized directly by Panda because it is the most-derived class.

The attempts by intermediate constructors to initialize the virtual base are ignored when they are not the most-derived class.


Construction Order with Virtual Bases

The order is broadly:

virtual base classes

other direct base classes

derived-class members

derived constructor body

Virtual bases are initialized before nonvirtual bases.

The detailed order follows the hierarchy and derivation lists, not the written order of initializer expressions.


Destruction Order

Destruction occurs in reverse.

derived object

ordinary bases

virtual base

The shared virtual base is destroyed once.


Essential Study Checklist

  1. Exception handling separates run-time error detection from error handling.
  2. Throwing an exception starts stack unwinding and destroys local objects in exited scopes.
  3. An uncaught exception leads to std::terminate().
  4. Class-type exceptions should normally be caught by reference, especially when inheritance is involved.
  5. Catch handlers are tried in source order, so derived exception handlers must appear before base handlers.
  6. throw; rethrows the currently handled exception, and catch (...) matches every exception.
  7. A constructor function try block is required to catch exceptions from base or member initialization.
  8. noexcept promises that an exception will not escape; violating that promise calls std::terminate().
  9. The noexcept operator tests at compile time whether an expression is declared nonthrowing.
  10. Standard exceptions form an inheritance hierarchy rooted at std::exception, whose what() function describes the error.
  11. A namespace is a named scope used to organize code and prevent collisions between independently developed libraries.
  12. Namespaces can be reopened, nested, aliased, and split across multiple files.
  13. An unnamed namespace provides translation-unit-local names and is preferred to old file-scope static usage.
  14. A using declaration introduces a selected name; a using directive makes all names from a namespace available for unqualified lookup.
  15. Argument-dependent lookup searches namespaces associated with class-type function arguments.
  16. using std::swap; swap(a, b); allows ADL to find a type-specific swap.
  17. Multiple inheritance gives a class more than one direct base and can create ambiguous names and conversions.
  18. In multiple inheritance, bases are initialized in derivation-list order and destroyed in reverse order.
  19. If the same base is inherited through multiple nonvirtual paths, the most-derived object contains multiple base subobjects.
  20. Virtual inheritance makes multiple inheritance paths share one virtual-base subobject.
  21. Virtual inheritance removes duplicate virtual-base subobjects but does not automatically remove all name ambiguities.
  22. The most-derived constructor is responsible for initializing virtual base classes.
  23. Virtual bases are initialized before ordinary nonvirtual bases.