본문으로 건너뛰기

Specialized Tools and Techniques

C++ provides several less commonly used facilities for specialized low-level and systems programming.

This chapter focuses on:

custom memory allocation
run-time type identification
enumerations
pointers to class members
nested and local classes
unions
nonportable low-level features

19.1 Controlling Memory Allocation

Some programs need more control over how dynamic memory is obtained and released.

C++ allows programs to customize allocation through:

operator new
operator new[]
operator delete
operator delete[]

These functions manage raw storage.

Object construction and destruction are separate steps.


19.1.1 Overloading new and delete

A new expression conceptually performs three steps:

allocate raw storage

construct object

return pointer

Example:

std::string* pointer =
new std::string("hello");

The allocation step calls:

operator new

and the compiler then constructs the std::string.

A delete expression performs the reverse process:

destroy object

release raw storage
delete pointer;

Allocation Functions

Programs can provide their own allocation functions.

Typical forms are:

void* operator new(
std::size_t size);

void operator delete(
void* memory);

Array allocation uses:

operator new[]
operator delete[]

A custom allocator must return suitably aligned raw storage or report allocation failure correctly.


Class-Specific Allocation

A class can define its own allocation functions.

class Widget
{
public:
static void* operator new(
std::size_t size);

static void operator delete(
void* memory);
};

Then:

Widget* pointer =
new Widget;

first looks for a class-specific operator new.

If no suitable class member is found, the global allocation function is used.


Global Allocation Replacement

Defining global versions of:

operator new
operator delete

can affect dynamic allocation throughout the program.

Such replacements must be implemented very carefully because they become part of the program's fundamental memory-management mechanism.


19.1.2 Placement new Expressions

Placement new: Constructs an object in memory that has already been allocated.

Required header:

#include <new>

Example:

void* raw =
::operator new(
sizeof(Widget)
);

Widget* pointer =
new (raw) Widget();

The placement expression:

new (raw) Widget()

does not allocate another block.

It constructs a Widget at the address stored in raw.


Manual Destruction

When object lifetime is managed manually, destruction and storage release are separate.

pointer->~Widget();

::operator delete(raw);

Conceptually:

raw storage
↓ placement new
live object
↓ explicit destructor
raw storage
↓ deallocation
released storage

Placement new is useful in allocators and containers that separate storage allocation from object construction.


19.2 Run-Time Type Identification

Run-Time Type Identification (RTTI): Determines information about the actual type of an object while the program is running.

The two main RTTI facilities are:

dynamic_cast
typeid

RTTI is mainly useful when code operates through base-class pointers or references but needs information about the actual derived type.

When virtual functions can express the required behavior, virtual dispatch is usually preferable.


19.2.1 The dynamic_cast Operator

dynamic_cast performs a checked run-time conversion between related class types.

Common forms are:

dynamic_cast<Derived*>(pointer)

dynamic_cast<Derived&>(reference)

Pointer dynamic_cast

Suppose:

class Base
{
public:
virtual ~Base() = default;
};

class Derived
: public Base
{
};

A checked downcast can be written as:

Base* base =
get_object();

if (Derived* derived =
dynamic_cast<Derived*>(
base))
{
// base actually refers
// to a Derived object
}

If the conversion fails:

result == nullptr

A null source pointer also produces a null result.


Reference dynamic_cast

A reference cannot be null.

Therefore a failed reference cast throws:

std::bad_cast

Example:

try
{
Derived& derived =
dynamic_cast<Derived&>(
base_reference
);
}
catch (
const std::bad_cast& error)
{
// conversion failed
}

Why dynamic_cast Requires Care

Using dynamic_cast means the caller must know which derived type it expects.

Whenever possible:

virtual function
preferred

manual run-time type test
specialized use

19.2.2 The typeid Operator

typeid: Returns information describing the type of an expression or type name.

Required header:

#include <typeinfo>

Example:

if (typeid(object)
== typeid(Derived))
{
// exact type is Derived
}

The result is a reference to:

const std::type_info

Static and Dynamic Type

For a nonpolymorphic expression, typeid reports the expression's static type.

For a polymorphic class expression, typeid can report the object's dynamic type.

Base& reference =
derived_object;

typeid(reference)

can identify the actual derived object when Base is polymorphic.


Dereferencing a Null Pointer

For a polymorphic type:

typeid(*pointer)

may need the dynamic type of the pointed-to object.

If the pointer is null, the operation throws:

std::bad_typeid

19.2.3 Using RTTI

RTTI is useful when an operation depends on the exact dynamic types of two or more objects and cannot naturally be represented as a virtual member function.

A typical pattern is:

if (typeid(lhs)
== typeid(rhs))
{
// same dynamic type
}

or:

if (auto pointer =
dynamic_cast<
const Derived*
>(&object))
{
// Derived-specific work
}

Use RTTI only when the design truly requires explicit type inspection.


19.2.4 The type_info Class

typeid returns an object of type:

std::type_info

Useful operations include:

OperationMeaning
t1 == t2Same represented type
t1 != t2Different represented types
t.name()Implementation-defined type name
t.before(u)Implementation-defined ordering

Example:

const std::type_info& info =
typeid(object);

std::cout
<< info.name();

The result of name() is implementation dependent and should not be treated as a portable class-name format.


19.3 Enumerations

Enumeration: A distinct type whose values are a fixed set of named constants.

C++11 provides:

unscoped enumeration
scoped enumeration

Unscoped Enumeration

enum Color
{
red,
yellow,
green
};

The enumerator names are introduced into the surrounding scope.

Color color =
red;

Unscoped enumerators can be converted to integral types.


Scoped Enumeration

enum class Color
{
red,
yellow,
green
};

Enumerators must be qualified:

Color color =
Color::red;

Scoped enumerations do not implicitly convert their values to integers.

This gives stronger type safety.


Enumerator Values

Explicit values may be supplied:

enum Status
{
idle = 0,
running = 10,
stopped = 20
};

If an initializer is omitted, the value is one greater than the preceding enumerator.

enum Number
{
zero,
one,
two
};

Conceptually:

zero = 0
one = 1
two = 2

Enumerators are constant expressions.


Underlying Type

An enumeration is represented by an integral type.

C++11 allows that type to be specified explicitly.

enum class Mode
: unsigned char
{
read,
write
};

A scoped enumeration defaults to:

int

when no underlying type is written.


Forward Declaration

An enumeration can be forward declared when its underlying type is known.

Scoped:

enum class Mode;

Unscoped:

enum Token
: unsigned int;

All declarations and the definition must agree about:

scoped or unscoped form
underlying type

19.4 Pointer to Class Member

Pointer to Member: A pointer-like value that identifies a nonstatic member of a class rather than a member inside one particular object.

The pointer records:

class type
+
member type
+
which member

An object is supplied later when the pointer is used.

Static members use ordinary pointers because they do not belong to individual objects.


19.4.1 Pointers to Data Members

Suppose:

class Screen
{
public:
std::string contents;
};

A pointer to the data member can be formed with:

auto data =
&Screen::contents;

Its explicit type is:

std::string Screen::*

Using .*

For an object:

Screen screen;

screen.*data =
"hello";

The operator:

.*

applies a pointer-to-member to an object.


Using ->*

For a pointer to an object:

Screen* pointer =
&screen;

pointer->*data =
"world";

The operator:

->*

applies a pointer-to-member through an object pointer.

Normal class access control still applies when forming pointers to members.


19.4.2 Pointers to Member Functions

A pointer can also identify a nonstatic member function.

class Screen
{
public:
char get() const;
};

Create the pointer:

auto function =
&Screen::get;

Unlike ordinary functions, the address-of operator must be written explicitly for a member-function pointer.


Calling through a Member-Function Pointer

For an object:

Screen screen;

char value =
(screen.*function)();

For an object pointer:

Screen* pointer =
&screen;

char value =
(pointer->*function)();

The parentheses are required because of operator precedence.


Overloaded Member Functions

If the member function is overloaded, its exact type must identify the desired overload.

char (
Screen::*function
)(
Screen::pos,
Screen::pos
) const =
&Screen::get;

The pointer type must include relevant:

return type
parameter types
class type
const qualification
reference qualification

19.4.3 Using Member Functions as Callable Objects

Standard-library adapters can turn member-function pointers into ordinary callable objects.


std::function

std::function<
bool(
const std::string&
)
> empty =
&std::string::empty;

Now:

bool result =
empty(text);

The wrapper supplies the object as an explicit function argument.


std::mem_fn

std::mem_fn deduces the member-pointer type automatically.

auto empty =
std::mem_fn(
&std::string::empty
);

Usage:

empty(text);

empty(&text);

The generated callable can handle either an object or a pointer to an object.


std::bind

A member function can also be adapted with bind.

using namespace
std::placeholders;

auto empty =
std::bind(
&std::string::empty,
_1
);

For direct member-pointer adaptation, mem_fn is often simpler because the compiler deduces the callable interface.


19.5 Nested Classes

Nested Class: A class declared inside another class.

Nested classes are often used for implementation types that logically belong to an enclosing class.

class TextQuery
{
public:
class QueryResult;
};

QueryResult is a type member of TextQuery.


Defining a Nested Class outside Its Enclosing Class

A nested class may be declared inside and defined later.

class TextQuery::QueryResult
{
// ...
};

Until the definition is seen, the nested class is an incomplete type.


Access to the Nested Type

The access section containing the declaration controls who can name the nested type.

public
available to ordinary users

protected
available to derived classes
and friends

private
available only according
to private access rules

Nested Objects Are Independent

An object of the nested class does not automatically contain an object of the enclosing class.

Likewise, an enclosing object does not automatically contain a nested-class object.

Nesting mainly affects:

scope
name lookup
accessibility of the nested type

19.6 union: A Space-Saving Class

union: A class type whose members share the same storage.

Only one member value is normally active at a time.

union Token
{
char character;
int integer;
double floating;
};

Conceptually:

one storage region

character
integer
floating
all overlap

The union occupies enough storage for its largest member, subject to alignment requirements.


Active Member

Token token;

token.integer =
42;

Now the active value is the integer member.

Assigning another member changes which value the program treats as active.


Anonymous Union

An anonymous union has no type or object name.

union
{
char character;
int integer;
};

Its members are accessed directly in the surrounding scope.

An anonymous union cannot define member functions or have private or protected members.


Class-Type Members

C++11 permits unions to contain class-type members such as:

std::string

However, when switching to or from a nontrivial class member, the program must explicitly manage that member's lifetime.

Conceptually:

construct new active member

use it

destroy it before
activating incompatible member

Placement new is commonly used to construct a class-type union member.


Discriminant

A class that manages a union commonly stores a separate value recording which member is active.

This value is called a discriminant.

enum class Kind
{
integer,
floating
};

class Token
{
private:
Kind kind;

union
{
int integer;
double floating;
};
};

The class must keep:

discriminant
and
active union member

synchronized.


19.7 Local Classes

Local Class: A class defined inside a function body.

void function()
{
class Local
{
public:
void run()
{
}
};
}

The class name is visible only within the enclosing scope.


Restrictions on Local Classes

A local class:

  • must define its member functions inside the class body
  • cannot have static data members
  • has only limited access to names from the enclosing function

It may use enclosing:

type names
static variables
enumerators

but not ordinary automatic local variables.


Example

void function()
{
static int shared = 0;
int local = 0;

class Local
{
public:
void run()
{
++shared;

// Error:
// ++local;
}
};
}

Local classes are generally useful only for small implementation details.


19.8 Inherently Nonportable Features

Some C++ facilities depend strongly on:

compiler
machine architecture
ABI
hardware
operating system

Programs using these features may require changes when moved to another platform.


19.8.1 Bit-fields

Bit-field: An integral or enumeration data member stored using a specified number of bits.

using Bit =
unsigned int;

class File
{
Bit mode : 2;
Bit modified : 1;
};

The number after : specifies the field width.


Bit-Field Properties

Bit-fields are often used for:

  • compact binary representations
  • hardware interfaces
  • externally defined binary formats

However, their exact memory layout is implementation dependent.


Prefer Unsigned Types

Signed bit-field behavior is implementation defined.

Therefore unsigned integral types are usually preferred.


No Address of a Bit-Field

The address-of operator cannot be applied to an individual bit-field.

// Error:
// auto pointer = &file.modified;

Therefore an ordinary pointer cannot refer directly to one bit-field.


Bitwise Operations

Multi-bit fields are commonly manipulated with bitwise operators.

mode |= READ;

if (mode & WRITE)
{
// write mode enabled
}

19.8.2 volatile Qualifier

volatile: A type qualifier used when an object's value may change in ways outside ordinary program control.

Typical examples include:

  • memory-mapped hardware registers
  • values changed by external hardware
  • implementation-specific low-level interfaces
volatile int
display_register;

The compiler must treat accesses to volatile objects according to implementation-defined volatile semantics rather than assuming the value changes only through ordinary program statements.


Pointer Forms

As with const, volatility can apply to either the pointed-to object or the pointer itself.

volatile int value;

volatile int* pointer =
&value;

int* volatile
fixed_pointer =
nullptr;

An object may also be both:

const volatile

Volatile Member Functions

Only appropriately volatile-qualified member functions may be called on volatile class objects.

class Device
{
public:
int read() volatile;
};

volatile Is Not Thread Synchronization

volatile does not provide:

atomic operations
mutual exclusion
inter-thread ordering

Use the C++ concurrency facilities, such as atomics or synchronization primitives, for communication between threads.


Copy Control and volatile

Compiler-generated copy and assignment operations usually take nonvolatile references.

Therefore they cannot normally copy directly from volatile objects.

A class that needs such behavior must define appropriate volatile-qualified overloads explicitly.


19.8.3 Linkage Directives: extern "C"

C++ programs sometimes call functions implemented in another programming language, especially C.

A linkage directive tells the compiler that a declaration uses another language's linkage convention.


Single Declaration

extern "C"
std::size_t strlen(
const char*);

The function has C linkage.


Compound Linkage Directive

Several declarations can share the same linkage.

extern "C"
{
void initialize();
void shutdown();
}

Linkage directives must appear at namespace scope rather than inside class or function definitions.


C Header Pattern

When a header must be usable by both C and C++ code, a common pattern is:

#ifdef __cplusplus
extern "C"
{
#endif

void c_function(void);

#ifdef __cplusplus
}
#endif

The C compiler sees an ordinary C declaration.

The C++ compiler applies C linkage.


Portability Requirement

Calling code written in another language depends on compatible:

compilers
calling conventions
object formats
ABIs

extern "C" handles language linkage at the C++ level, but it does not make arbitrary binary interfaces automatically compatible.


Essential Study Checklist

  1. A new expression allocates raw storage and then constructs an object; delete destroys the object and then releases the storage.
  2. operator new and operator delete manage raw memory and can be replaced globally or defined for a specific class.
  3. Class-specific allocation functions are searched before global allocation functions for objects of that class.
  4. Placement new constructs an object in already allocated storage and does not itself obtain a new block.
  5. Manually managed placement-new objects must be destroyed before their storage is released.
  6. RTTI consists mainly of dynamic_cast and typeid, but virtual functions should usually be preferred when they model the operation naturally.
  7. A failed pointer dynamic_cast returns null, whereas a failed reference dynamic_cast throws std::bad_cast.
  8. typeid can report the dynamic type of a polymorphic object and returns a const std::type_info&.
  9. Scoped enumerations require qualified enumerator names and do not implicitly convert to integers.
  10. An enum may specify its underlying integral type, and C++11 permits enum forward declarations when the underlying size is known.
  11. A pointer to member identifies a class member independently of any particular object.
  12. .* applies a pointer-to-member to an object, while ->* applies it through an object pointer.
  13. A pointer to member function records the class, return type, parameters, and relevant member-function qualifiers.
  14. std::function, std::mem_fn, and std::bind can adapt pointers to member functions into callable objects.
  15. A nested class is a type member of its enclosing class and may be declared inside and defined later with a qualified name.
  16. A union lets several members share one storage region, with normally one active value at a time.
  17. Nontrivial class-type union members require explicit lifetime management when the active member changes.
  18. A discriminant records which union member is currently active.
  19. A local class is defined inside a function, has local scope, cannot have static data members, and has restricted access to enclosing automatic variables.
  20. Bit-fields store integral or enumeration members using specified bit widths, but their layout is implementation dependent.
  21. Unsigned types are normally preferred for bit-fields, and the address of a bit-field cannot be taken.
  22. volatile is intended for implementation-specific externally changing objects such as hardware interfaces; it is not a thread-synchronization mechanism.
  23. Volatile objects require appropriately qualified pointers, references, and member functions.
  24. extern "C" declares C language linkage and is used when C++ code interfaces with compatible C code.
  25. Memory layout, bit-fields, volatile behavior, and foreign-language linkage are platform-sensitive and should be isolated when portability matters.