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