본문으로 건너뛰기

Ground Rules

대규모 C++ 프로젝트에서 유지보수성과 확장성을 확보하기 위한 기본 설계 규칙을 정리합니다.

2.1 Overview

Design Rule:
A practice that should be followed without exception in a large project.

객관적으로 확인할 수 있을 정도로 명확하고 구체적이어야 한다.

Major Design Rule:
A design rule whose violation can affect other components and the overall system.

대규모 프로젝트에서는 원칙적으로 항상 지켜야 한다.

Minor Design Rule:
A strongly recommended rule whose isolated violation is unlikely to affect the entire system.

국소적인 위반의 영향은 비교적 제한적이다.

Guideline:
A recommended practice that may be ignored when there is a compelling engineering reason.

Principle:
A useful design observation that must be evaluated in the context of a particular design.

Major Design Rule

Must be followed

Minor Design Rule

Strongly recommended

Guideline

Follow unless there is a good reason not to

Principle

Apply according to context

2.2 Member Data Access

Encapsulation:
Hiding implementation details behind a procedural interface.

A contained implementation detail is encapsulated when clients cannot access or detect it through the logical interface.

Major Design Rule

Keep class data members private.

class Rectangle
{
Point d_lowerLeft;
int d_width;
int d_height;

public:
void moveBy(const Point& delta);

Point getLowerLeft() const;
Point getUpperRight() const;
};

Private data allows the internal representation to change without forcing clients to modify their code.

Accessor:
A member function that provides controlled read access to object state.

Manipulator:
A member function that modifies object state through the class interface.

Client

Public Interface

Private Data

Public Data

Public data exposes the physical representation of a class.

class Rectangle
{
public:
Point d_lowerLeft;
Point d_upperRight;
};

Changing the representation can therefore require changes to client code.

Protected Data

Protected data also weakens encapsulation because derived classes can depend directly on the representation.

Prefer private data with protected member functions when derived classes require controlled access.

private data
+
protected functions

Better encapsulation

2.3 The Global Name Space

Global Name Space:
The file-level naming space in which independently developed names may collide when components are integrated.

Large projects should minimize the number of identifiers introduced into global scope.

2.3.1 Global Data

Global Data:
Data defined at file scope and directly accessible outside its local abstraction.

Major Design Rule

Avoid data with external linkage at file scope.

int size;
double scale;

Global data increases:

  • Name collisions
  • Coupling
  • Testing difficulty
  • Debugging difficulty
  • Reuse difficulty

Prefer encapsulating shared state inside a class.

class Global
{
static int s_size;

public:
static void setSize(int size);
static int getSize();
};

Global Module:
A class containing private static state accessed through static member functions.

Use globally accessible modules only when the represented entity is inherently global.

2.3.2 Free Functions

Free Function:
A function that is not a member of a class.

Major Design Rule

Avoid free functions at file scope when they can unnecessarily pollute the global name space.

The book allows free operator functions as an important exception.

Instead of unrelated global utility functions:

int getMonitorResolution();
void setSystemScale(double scale);

group them within an appropriate class or structure.

struct SysUtil
{
static int getMonitorResolution();
static void setSystemScale(double scale);
};

This reduces the number of names exposed globally.

2.3.3 Enumerations, Typedefs, and Constant Data

Major Design Rule

Avoid enumerations, typedefs, and constants at file scope in header files.

File-scope names can collide with unrelated names from other components.

Bad:

enum Color
{
RED,
GREEN,
ORANGE
};

typedef long BigInt;

const int DEFAULT_SIZE = 100;

Prefer class scope.

class Paint
{
public:
enum Color
{
RED,
GREEN,
ORANGE
};

typedef long BigInt;
};

Scoped names reduce collisions.

Paint::ORANGE

Name Qualification:
Specifying the scope containing a name in order to distinguish it from other names.

Global Name

High collision risk

Class::Name

Restricted scope

2.3.4 Preprocessor Macros

Preprocessor Macro:
A textual substitution performed before C++ compilation.

#define BUFFER_SIZE 100

Macros do not obey normal C++ scope rules.

Major Design Rule

Avoid preprocessor macros in header files except as include guards.

Problems with macros include:

  • No C++ scope
  • Name collisions
  • Difficult debugging
  • Reduced readability
  • Poor tool support

Use normal C++ language features whenever possible.

2.3.5 Names in Header Files

A file-scope name declared in a header can potentially collide with names throughout the system.

Major Design Rule

At file scope in a header, the book recommends declaring only:

  • Classes
  • Structures
  • Unions
  • Free operator functions

Definitions at file scope should generally be limited to:

  • Classes
  • Structures
  • Unions
  • Inline member functions
  • Inline free operator functions

Good:

class Driver;

struct DriverInit;

class Driver
{
enum Color
{
RED,
GREEN
};

typedef int Value;

int d_size;

public:
void setSize(int size);
};

Avoid unrelated file-scope data, typedefs, enumerations, constants, macros, and ordinary free functions.

2.4 Include Guards

Include Guard:
A preprocessor mechanism that prevents the contents of a header from being processed more than once in one translation unit.

Without an include guard:

c.h
├── a.h
└── b.h
└── a.h

a.h included twice

Repeated definitions can produce compilation errors.

Major Design Rule

Place a unique and predictable include guard around every header file.

#ifndef INCLUDED_STACK
#define INCLUDED_STACK

class Stack
{
};

#endif

An include guard ensures that the contents of a header are incorporated at most once in each translation unit.

First include

Guard undefined

Process header

Later include

Guard already defined

Skip header

2.5 Redundant Include Guards

Redundant Include Guard:
An additional guard placed around an #include directive to avoid reopening and reprocessing an already included header.

#ifndef INCLUDED_WIDGET
#include "widget.h"
#endif

Minor Design Rule

Place redundant include guards around include directives in header files.

The purpose is compile-time efficiency rather than correctness.

Internal Include Guard

Prevents duplicate definitions

Redundant Include Guard

Prevents unnecessary header processing

Dense include graphs can cause repeated preprocessing.

The book notes that redundant guards can prevent compile-time behavior from approaching quadratic growth in large include graphs.

They are generally unnecessary around includes in implementation files.

2.6 Documentation

Interface Documentation:
Documentation that explains how clients are expected to use an interface.

Guideline

Document interfaces so that other developers can use them without examining the implementation.

Have at least one other developer review each interface.

A client should normally be able to understand a component from its header and interface documentation.

Undefined Behavior

Undefined Behavior:
Behavior for which the interface provides no required result.

Guideline

Explicitly document conditions under which behavior is undefined.

struct MathUtil
{
static int factorial(int n);

// Returns n! for n >= 0.
// Behavior is undefined for negative n
// or when the result cannot fit in int.
};

If undefined conditions are not documented, clients may accidentally depend on implementation-specific behavior.

Assertions and Documentation

Principle

Assertions can document assumptions made by an implementation.

int process(const char* data)
{
assert(data);

// ...
}

Documentation specifies the contract.

assert verifies assumptions during development.

Documentation

Defines expected use

assert

Checks implementation assumptions

2.7 Identifier-Naming Conventions

Consistent naming makes large codebases easier to read and maintain.

Data Members

Minor Design Rule

Use a consistent method to identify class data members.

The book uses:

int d_size;

d_ identifies instance data.

For static data:

static int s_count;

s_ identifies static class data.

Type Names

Minor Design Rule

Use a consistent method to distinguish type names.

Example:

class Rectangle;
struct Point;

The book recommends beginning type names with an uppercase letter.

Constants

Minor Design Rule

Use a consistent method to identify immutable values.

Example:

const int MAX_SIZE = 100;

enum
{
DEFAULT_SIZE = 10
};

The book recommends uppercase letters with underscores.

Multi-Word Identifiers

Guideline

Use one consistent convention for separating words.

For example:

getUpperRight()

or

get_upper_right()

Do not arbitrarily mix naming conventions.

Recurring Interfaces

Guideline

Use consistent names for operations that serve the same purpose.

Recurring abstractions such as iterators should expose similar operations using consistent terminology.

2.8 Key Rules

Major Rules

  1. Keep class data members private.
  2. Avoid file-scope data with external linkage.
  3. Minimize free functions in the global name space.
  4. Avoid file-scope enumerations, typedefs, and constants in headers.
  5. Avoid macros in headers except include guards.
  6. Restrict what is introduced at file scope in headers.
  7. Give every header a unique include guard.

Minor Rules

  1. Use redundant include guards in header files.
  2. Use consistent prefixes for data members.
  3. Distinguish type names consistently.
  4. Distinguish immutable values consistently.

Core Principle

Large-Scale C++ Design

Encapsulation

Hide representation

Limited Global Scope

Avoid name collisions

Controlled Headers

Reduce dependencies

Include Guards

Safe inclusion

Documentation

Define interface contracts

Naming Conventions

Improve consistency