본문으로 건너뛰기

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