본문으로 건너뛰기

Implementing an Object

This chapter covers implementation-level decisions for C++ objects, including data layout, function bodies, dynamic memory management, templates, and design patterns.

10.1 Member Data

Member-data choices affect:

  • Object size.
  • Alignment.
  • Runtime performance.
  • Portability.
  • Maintainability.

10.1.1 Natural Alignment

Natural Alignment:
A fundamental object is naturally aligned when its size divides its numerical memory address.

Typical example on a 32-bit machine:

char
└── 1-byte boundary

short
└── 2-byte boundary

int / pointer
└── 4-byte boundary

double
└── Often 8-byte boundary

Aggregate Natural Alignment:
An aggregate object is naturally aligned when it satisfies the most restrictive alignment requirement of its embedded types.

Padding

Padding:
Unused bytes inserted by the implementation to satisfy alignment requirements.

Padding can occur:

  • Between data members.
  • At the end of an object.

It does not occur before the first data member.

Principle:
The order in which data members are declared can affect object size.

Example:

struct A
{
int d_i1;
double d_d;
int d_i2;
};

may require more space than:

struct B
{
int d_i1;
int d_i2;
double d_d;
};

because the second organization can reduce alignment holes.

Poor Ordering

More Padding

Larger Object

Better Ordering

Less Padding

Smaller Object

Member ordering becomes particularly important when many instances of the class are active simultaneously.

Placement and Alignment

When constructing an object using placement new, the supplied address must satisfy the alignment requirement of the object.

new(address) T;

Using an incorrectly aligned address can cause invalid behavior on architectures that require natural alignment.

10.1.2 Fundamental Types Used in the Implementation

Unlike the public interface, implementation details may use narrower fundamental types when doing so is a proven optimization.

short

Guideline:
Use short instead of int in the implementation as an optimization only when it is known to be safe.

Example:

class Point
{
short d_x;
short d_y;

public:
Point(int x, int y);
};

Using short may be reasonable when the valid range is known and guaranteed.

Do not use a narrower type merely because current values happen to be small.

unsigned

Guideline:
Consider not using unsigned even in the implementation.

Principle:
Using unsigned to gain one extra bit indicates that the fundamental integral type is not large enough to be safe.

Need Extra Bit from unsigned

Underlying Type Probably Too Small

Optimization using short, char, or similar types should be deferred until:

  1. The implementation works.
  2. Functional tests pass.
  3. Performance measurements justify the change.
  4. Regression tests exist.

10.1.3 Using typedef in the Implementation

typedef:
A type alias that can isolate platform-dependent representation choices.

The book uses aliases for fixed-size types when exact representation width matters across platforms.

struct sys_Type
{
typedef signed char Int8;
typedef short Int16;
typedef int Int32;

typedef float Float32;
typedef double Float64;
};

The purpose is to centralize machine-specific assumptions.

Platform-Specific Fundamental Types

typedef Layer

Stable Implementation Types

When fixed-size aliases are used, their assumptions should be tested.

assert(sizeof(sys_Type::Int32) == 4);

10.2 Function Definitions

At the function-body level, design mistakes are comparatively localized.

Even so, several implementation practices improve reliability and maintainability.

10.2.1 Assert Yourself

Assertions can document and verify implementation assumptions.

assert(pointer);
assert(size > 0);

Use assertions for:

  • Preconditions.
  • Internal invariants.
  • States that should be impossible.
  • Assumptions required by later code.
Comment
└── Passive Documentation

assert
└── Executable Documentation

Assertions also force the developer to state assumptions explicitly, which often makes the control flow easier to understand.

For systems that require recovery rather than immediate termination, the responsibility for handling programming errors may be escalated through exceptions.

10.2.2 Avoid Special Casing

Principle:
Algorithms that naturally include their boundary conditions are often simpler, shorter, easier to understand, and easier to test than algorithms that treat boundary conditions separately.

Bad structure:

Normal Case
+
Empty Case
+
First Element Case
+
Last Element Case

Prefer an algorithm in which these conditions naturally follow the same path.

Extra Indirection

Principle:
A variety of problems can be solved by adding an extra level of indirection.

For linked structures, maintaining the address of a pointer can eliminate special cases.

Link** current = &d_head_p;

This can allow insertion and removal at the first element to use the same logic as operations elsewhere in the list.

Pointer

Special Boundary Cases

Pointer-to-Pointer

Uniform Manipulation

10.2.3 Factor Instead of Duplicate

Inside one component, repeated implementation logic should normally be factored.

Typical shared operations include:

Default Constructor
└── init()

Copy Constructor
├── init()
└── copy()

Assignment
├── clean()
├── init()
└── copy()

Destructor
└── clean()

Principle:
Factoring generally reusable functionality within a component can reduce code size and improve reliability with only a modest loss in runtime performance.

Do not duplicate common search or traversal logic simply to avoid a small local function call.

Profile first before replacing factored code with specialized duplicated implementations.

10.2.4 Don't Be Too Clever

Guideline:
When designing a function, component, package, or entire system, use the simplest techniques that are effective.

Clever but Obscure

Harder to Read
Harder to Maintain
Harder to Test

Simple and Effective

Preferred

Knowledge of unusual language features is not a reason to use them.

10.3 Memory Management

Custom memory management can substantially improve runtime performance, but it can also harm an integrated system if applied without sufficient context.

The chapter distinguishes:

  • Logical and physical state.
  • Physical allocation parameters.
  • Block allocators.
  • Pool allocators.
  • Class-specific memory management.
  • Object-specific memory management.

10.3.1 Logical versus Physical State Values

Logical Value:
A state value that contributes to the intended semantics of an object.

Physical Value:
A state value that exists only because of a particular implementation choice.

Example for a string:

Logical State
├── String contents
└── Logical length

Physical State
├── Address of dynamic buffer
└── Allocated buffer capacity

Principle:
For a fully encapsulating interface, every programmatically accessible value is a logical value.

Guideline:
Avoid allowing programmatic access to physical values.

Hints

Hint:
Information supplied by a client that may help an implementation improve performance but does not affect required logical behavior.

Example:

String(const char* text,
int maxLengthHint = 0);

The implementation may:

  • Use the hint.
  • Ignore the hint.

Incorrect hints may reduce performance, but they must not change correctness.

Principle:
A hint is write-only.

There should be no programmatic way for clients to determine whether the implementation used the hint.

Principle:
The best hints are not tied directly to a specific implementation.

Prefer:

maxLengthHint

over an implementation-specific term such as:

bufferSize

Logical Constness

Guideline:
The result of calling a const member function should not alter any programmatically accessible value in the object.

Equality

Principle:
Two objects supporting value semantics are equal when all corresponding logical values are equal.

Physical values do not determine value equality.

Logical State Equal

Objects Equal

Physical Representation Different

May Still Be Equal

10.3.2 Physical Parameters

Physical Parameter:
An implementation value that controls allocation behavior or other performance characteristics without affecting logical semantics.

The expandable stack example uses:

  • INITIAL_SIZE
  • GROW_FACTOR

Growth by Factor

new_size = old_size × GROW_FACTOR

Geometric growth requires approximately:

O(log N)

reallocations to reach size N, while the total copying remains approximately:

O(N)

A growth factor of 2 means allocated capacity remains bounded to less than approximately twice the amount currently required.

Growth by Fixed Size

new_size = old_size + GROW_SIZE

Fixed-size growth can become very slow when actual size greatly exceeds the expected size.

General Preference

GROW_FACTOR
├── Adaptive
├── Robust over wide size range
└── Predictable relative excess capacity

GROW_SIZE
├── Potentially space-efficient in some patterns
└── Can exhibit poor scaling

The chapter recommends a small initial size and geometric growth as a strong initial implementation choice, followed by measurement and tuning if required.

10.3.3 Memory Allocators

The chapter introduces two basic allocator organizations.

Block Allocator

Block Allocator:
An object that tracks separately allocated blocks and can release all of them together.

Block Allocator
├── Block A
├── Block B
├── Block C
└── release()

Use when many allocations share a common lifetime.

Advantages:

  • Centralized ownership.
  • Bulk release.
  • Simplified cleanup.

Pool Allocator

Pool Allocator:
An allocator that manages a free list of fixed-size memory blocks.

Large Block

Split into Fixed-Size Chunks

Free List

alloc() / free()

A pool is useful when many objects of the same size are repeatedly created and destroyed.

Instrumenting new and delete

Principle:
Instrumenting global new and delete is a simple but effective way to understand and test dynamic-memory behavior.

Instrumentation can reveal:

  • Allocation counts.
  • Requested sizes.
  • Deallocation behavior.
  • Unexpected startup allocations.

Principle:
Using iostream while instrumenting global new and delete can cause undesirable side effects.

The book recommends simpler output facilities because stream initialization itself may allocate memory.

10.3.4 Class-Specific Memory Management

Class-Specific Memory Management:
Providing class-specific operator new and operator delete so all dynamic instances of a class use a specialized allocator.

class Entry
{
public:
void* operator new(size_t size);
void operator delete(void* address, size_t size);
};

A common implementation allocates many equal-sized chunks at once and maintains them in a static free list.

Global Allocator

Large Chunk

Class Static Free List

Individual Objects

Benefits

Class-specific allocation can:

  • Reduce calls to the global allocator.
  • Reduce per-allocation overhead.
  • Significantly improve runtime performance for frequently allocated objects.

Chunk Size

A larger CHUNK_SIZE amortizes the cost of global allocation across more local allocations.

Global Allocation Cost

Spread Across N Objects

Lower Cost per Object

Performance gains eventually show diminishing returns.

10.3.4.1 Adding Custom Memory Management

Class-specific allocation is particularly effective when:

  • Every instance has the same size.
  • Objects are allocated frequently.
  • Objects are deallocated frequently.
  • Global allocation is a significant runtime cost.

A free-list allocator typically performs:

operator new
├── Free list empty?
│ └── Replenish
└── Remove first free block

operator delete
└── Push block onto free list

10.3.4.2 Hogging Memory

A major weakness of class-specific pools is that memory is often retained for the entire life of the program.

Principle:
Class-specific allocation schemes that never give back their memory make automated detection of memory leaks much more difficult.

Static free lists may accumulate memory until they reach a historical high-water mark.

Phase 1
└── Class A Pool grows

Phase 2
└── Class B Pool grows

Phase 3
└── Class C Pool grows

Old Pools Keep Memory

Total Memory Keeps Increasing

Principle:
Class-specific memory allocators tend to soak up globally allocated memory, thereby increasing overall memory usage.

A pool may contain unused memory that is not technically leaked but is unavailable for use elsewhere.

Principle:
Indiscriminate class-specific memory management is a form of egocentric behavior that can adversely affect overall system performance.

The fundamental problem is lack of context: the class-level allocator does not know when a group of objects is no longer needed.

10.3.5 Object-Specific Memory Management

Object-Specific Memory Management:
A higher-level object owns an allocator and uses it to manage its subordinate objects.

Manager Object
├── Private Allocator
├── Subordinate Object A
├── Subordinate Object B
└── Subordinate Object C

Principle:
An object-specific allocator has enough context to know when the instances managed by a particular object are no longer required.

Guideline:
Prefer object-specific over class-specific memory management.

Ownership

A manager may hold its allocator through a pointer.

class Queue
{
Pool* d_allocator_p;
};

Guideline:
Use a non-const pointer data member to hold managed objects.

The manager controls:

  • Allocator lifetime.
  • Subordinate-object lifetime.
  • When memory can be returned to the system.

Allocation Responsibility

Class-Specific
└── Subordinate Class Manages Global Pool

Object-Specific
└── Manager Owns Private Pool
```

Escalating allocation responsibility to the manager prevents unrelated instances from sharing one static high-water mark.

### Initialization Order

**Minor Design Rule:**
Avoid depending on the order in which data members are defined in an object during initialization.

Data members are initialized in declaration order, not initializer-list order.

Avoid designs where one member initializer assumes another member has already been initialized unless declaration order is guaranteed as part of the design.

### Usage-Pattern Knowledge

**Principle:**
Knowledge of a particular client's usage pattern can enable a more effective allocator for its managed objects.

Examples:

- A symbol table may rarely delete strings individually.
- A manager may know that all subordinate objects die together.
- A block allocator can therefore reclaim the entire group at once.

### Testing Optimized Allocation

**Guideline:**
Consider providing a way to switch between block allocation and individual allocation.

Benefits:

1. Standard memory-analysis techniques can expose allocation errors.
2. The actual time and space benefit of the optimization can be measured.

### Memory Leak

**Memory Leak:**
A condition in which a program loses the ability to free dynamically allocated memory.

A block allocator may mask certain allocation mistakes because it still owns the underlying block even when the manager has lost the use of an individual element.

Therefore optimized allocation should remain testable in an unoptimized mode.

## 10.4 Using C++ Templates in Large Projects

Templates provide powerful source-level reuse, especially for container types.

However, the chapter emphasizes two costs:

- Compiler/linker implementation complexity.
- Potential generation of redundant object code.

## 10.4.1 Compiler Implementations

The book describes two broad template implementation models.

### Repository / CFRONT-Like

~~~text
Template Definitions

System-Wide Repository

Repeated Instantiation / Link Resolution

Potential problem:

  • Very high link-time cost.

Macro-Like

Template Header
+
Template Implementation Source

Available to Client

Potential problem:

  • Weak insulation because implementation source must be visible.

The chapter emphasizes that template support can strongly affect development cost in large projects.

10.4.2 Managing Memory in Templates

A general container template must work for both:

  • Fundamental types.
  • User-defined types.

This makes implementation more difficult than writing a container for one specific type.

Dummy Wrapper Type

Principle:
Embedding the actual parameter type inside a dummy parameterized class allows fundamental types to be treated like user-defined types where inheritance or class-specific behavior is needed.

template <class T>
struct StackItem
{
T d_item;
};

Bitwise Copy

Principle:
In general, an object cannot be copied or moved safely using a bitwise copy such as memcpy.

Bad:

memcpy(destination, source, sizeof(T));

A user-defined object may contain:

  • Owned pointers.
  • Self-references.
  • Other representation invariants.

Bitwise copying can therefore violate ownership and object semantics.

Assignment into Uninitialized Memory

Principle:
In general, an object cannot be copied or moved into uninitialized memory using its assignment operator.

Bad:

buffer[i] = source[i];

when buffer[i] has not yet been constructed.

Minor Design Rule:
When implementing memory management for a general container template, do not use the contained type's assignment operator when the destination is uninitialized memory.

Minimum Required Semantics

Guideline:
For a completely general container that manages memory for its objects, assume only that the parameter type provides:

  • Copy constructor.
  • Destructor.
General Parameter Type T
├── Copy Construction
└── Destruction

Do not assume:

  • Bitwise copy is valid.
  • Default construction is cheap or available.
  • Assignment exists.
  • A custom address operator behaves normally.

Placement Construction

Construct objects in raw storage using placement syntax.

new(address) T(source);

Destroy active objects explicitly.

object.T::~T();

Only actually constructed objects should be destroyed.

Developing a Template

The book suggests first replacing the concrete element type with a temporary alias and testing against several representative types before converting the implementation to template syntax.

Test with:

  • More than one fundamental type.
  • At least one heavyweight user-defined type.

This helps expose hidden assumptions about T.

Factoring Template Implementations

Guideline:
Where possible, implement templates on top of a factored reusable void* pointer type and use inline template functions to reestablish type safety.

Template<T>

Thin Type-Safe Inline Layer

Generic Pointer Implementation

This reduces duplicate object code generated for each template instantiation.

10.4.3 Patterns versus Templates

Templates capture reusable implementation structures that can be expressed concretely in terms of parameterized types.

Not all reusable design knowledge fits that model.

Design Pattern:
An abstract organization of classes or objects that has repeatedly proven effective for solving similar kinds of problems in different applications.

Template
└── Reusable Concrete Generic Code

Design Pattern
└── Reusable Architectural Organization

Examples discussed throughout the book include:

  • Utility class.
  • Protocol class.
  • Fully insulating class.
  • Iterator.

Principle:
Design patterns are an effective way of communicating reusable concepts and ideas at an architectural level.

Patterns provide a vocabulary for communicating recurring design structures.

Principle:
Design patterns, like the design process itself, address both logical and physical issues.

Logical Design Pattern

Addresses logical object relationships and behavior.

Example:

Iterator

Physical Design Pattern

Addresses source organization, dependencies, insulation, and levelization.

Example:

Component
├── Header
└── Implementation
```

## 10.5 Key Concepts

~~~text
Object Implementation

Localized Design Decisions

Member Data
├── Natural Alignment
├── Padding
├── Member Ordering
└── Fixed-Size typedefs

Function Implementation
├── Assertions
├── Avoid Special Cases
├── Factor Common Logic
└── Prefer Simple Techniques

Object State
├── Logical Values
└── Physical Values

Hint
├── Performance Suggestion
├── Write-Only
└── Must Not Affect Semantics

Expandable Storage
├── GROW_FACTOR
└── GROW_SIZE

Memory Allocators
├── Block Allocator
└── Pool Allocator

Custom Allocation
├── Class-Specific
│ ├── Fast
│ └── Can Hog Global Memory

└── Object-Specific
├── Manager Has Context
├── Better Lifetime Control
└── Preferred

Template Memory Rules
├── No General memcpy
├── No Assignment into Raw Storage
├── Copy Construct
├── Explicitly Destroy
└── Factor Generic Pointer Logic

Reusable Design
├── Templates
└── Design Patterns