C++ One Definition Rule | "단일 정의 규칙" 가이드

C++ One Definition Rule | "단일 정의 규칙" 가이드

이 글의 핵심

C++ One Definition Rule에 대한 실전 가이드입니다.

들어가며

ODR(One Definition Rule)은 C++의 핵심 규칙으로, 변수, 함수, 클래스 등이 전체 프로그램에서 하나의 정의만 가져야 한다는 원칙입니다. ODR을 이해하면 링크 에러를 예방하고 안전한 코드를 작성할 수 있습니다.


1. ODR 기본 규칙

변수의 ODR

// ❌ ODR 위반
// file1.cpp
int globalVar = 10;

// file2.cpp
int globalVar = 20;  // 중복 정의!

// 링크 에러: multiple definition of 'globalVar'
// ✅ 올바른 방법
// header.h
extern int globalVar;  // 선언

// file1.cpp
int globalVar = 10;  // 정의 (한 곳에서만)

// file2.cpp
#include "header.h"  // 선언만 포함

함수의 ODR

// ❌ ODR 위반
// file1.cpp
void func() {
    std::cout << "file1" << std::endl;
}

// file2.cpp
void func() {
    std::cout << "file2" << std::endl;
}

// 링크 에러: multiple definition of 'func()'
// ✅ 올바른 방법
// header.h
void func();  // 선언

// file1.cpp
void func() {
    std::cout << "implementation" << std::endl;
}

2. ODR 예외

inline 함수

// ✅ inline 함수는 여러 번 정의 가능
// utils.h
inline int add(int a, int b) {
    return a + b;
}

// file1.cpp
#include "utils.h"  // add 정의 포함

// file2.cpp
#include "utils.h"  // add 정의 포함 (OK, 동일한 정의)

핵심: inline 함수는 모든 정의가 동일하면 여러 번역 단위에 정의될 수 있습니다.

템플릿

// ✅ 템플릿은 헤더에 정의
// stack.h
template<typename T>
class Stack {
public:
    void push(const T& value) {
        data.push_back(value);
    }
    
    T pop() {
        T value = data.back();
        data.pop_back();
        return value;
    }
    
private:
    std::vector<T> data;
};

// file1.cpp
#include "stack.h"
Stack<int> intStack;  // int 버전 인스턴스화

// file2.cpp
#include "stack.h"
Stack<int> intStack2;  // int 버전 인스턴스화 (OK)

constexpr 변수

// C++17 이전
// header.h
constexpr int MAX = 100;  // 각 번역 단위마다 복사

// C++17 이후
// header.h
inline constexpr int MAX = 100;  // 하나의 정의

3. 실전 예제

예제 1: 전역 변수 관리

// config.h
#ifndef CONFIG_H
#define CONFIG_H

// ❌ 헤더에 정의 (ODR 위반)
// int maxConnections = 100;

// ✅ 헤더에 선언
extern int maxConnections;
extern const char* appName;

// ✅ C++17 inline 변수
inline int maxRetries = 3;

#endif
// config.cpp
#include "config.h"

// 정의 (한 곳에서만)
int maxConnections = 100;
const char* appName = "MyApp";
// main.cpp
#include "config.h"
#include <iostream>

int main() {
    std::cout << "Max connections: " << maxConnections << std::endl;
    std::cout << "Max retries: " << maxRetries << std::endl;
    return 0;
}

예제 2: 클래스 정의

// widget.h
#ifndef WIDGET_H
#define WIDGET_H

class Widget {
public:
    Widget();
    void use();
    
private:
    int data;
};

#endif
// widget.cpp
#include "widget.h"
#include <iostream>

Widget::Widget() : data(0) {
    std::cout << "Widget 생성" << std::endl;
}

void Widget::use() {
    std::cout << "Widget 사용" << std::endl;
}
// file1.cpp
#include "widget.h"
Widget w1;  // OK

// file2.cpp
#include "widget.h"
Widget w2;  // OK (클래스 정의는 동일하면 OK)

예제 3: Static 멤버 변수

// counter.h
#ifndef COUNTER_H
#define COUNTER_H

class Counter {
public:
    static int count;  // 선언
    
    Counter() {
        ++count;
    }
};

// ❌ C++17 이전: 헤더에 정의 불가
// int Counter::count = 0;

// ✅ C++17: inline static
class Counter2 {
public:
    inline static int count = 0;  // 정의 가능
    
    Counter2() {
        ++count;
    }
};

#endif
// counter.cpp
#include "counter.h"

// C++17 이전: 소스 파일에 정의
int Counter::count = 0;

4. 자주 발생하는 문제

문제 1: 헤더에 함수 정의

// ❌ 비인라인 함수 정의 (ODR 위반)
// utils.h
void func() {
    std::cout << "Hello" << std::endl;
}

// file1.cpp, file2.cpp 모두 인클루드 시 링크 에러!
// ✅ 해결책 1: inline 추가
// utils.h
inline void func() {
    std::cout << "Hello" << std::endl;
}

// ✅ 해결책 2: 소스 파일로 이동
// utils.h
void func();

// utils.cpp
void func() {
    std::cout << "Hello" << std::endl;
}

문제 2: 다른 정의

// ❌ 다른 정의 (정의되지 않은 동작)
// file1.cpp
struct Data {
    int x;
};

void process(Data d) {
    std::cout << d.x << std::endl;
}

// file2.cpp
struct Data {
    double x;  // 다른 정의!
};

Data d{3.14};
process(d);  // 정의되지 않은 동작 (UB)

해결책: 헤더 파일에 클래스를 정의하여 모든 번역 단위에서 동일한 정의를 사용하세요.

문제 3: 익명 네임스페이스

// ❌ 헤더에 익명 네임스페이스
// header.h
namespace {
    int value = 10;  // 각 번역 단위마다 다른 value
}

// ✅ 명명된 네임스페이스
// header.h
namespace MyLib {
    extern int value;
}

// source.cpp
namespace MyLib {
    int value = 10;
}

문제 4: constexpr 변수

// C++17 이전
// header.h
constexpr int MAX = 100;  // 각 번역 단위마다 복사 (비효율)

// C++17 이후
// header.h
inline constexpr int MAX = 100;  // 하나의 정의 (효율적)

5. ODR 준수 패턴

패턴 1: 헤더-소스 분리

// math.h
#ifndef MATH_H
#define MATH_H

// 선언만
int add(int a, int b);
int multiply(int a, int b);

extern int globalCounter;

#endif
// math.cpp
#include "math.h"

// 정의
int add(int a, int b) {
    return a + b;
}

int multiply(int a, int b) {
    return a * b;
}

int globalCounter = 0;

패턴 2: inline 함수

// utils.h
#ifndef UTILS_H
#define UTILS_H

#include <iostream>

// inline 함수는 헤더에 정의 가능
inline void log(const std::string& msg) {
    std::cout << "[LOG] " << msg << std::endl;
}

inline int square(int x) {
    return x * x;
}

#endif

패턴 3: 템플릿 클래스

// container.h
#ifndef CONTAINER_H
#define CONTAINER_H

#include <vector>

template<typename T>
class Container {
public:
    void add(const T& item) {
        items.push_back(item);
    }
    
    size_t size() const {
        return items.size();
    }
    
private:
    std::vector<T> items;
};

#endif

6. 실전 예제: 라이브러리 설계

// logger.h
#ifndef LOGGER_H
#define LOGGER_H

#include <string>
#include <iostream>

class Logger {
public:
    // 선언
    static Logger& getInstance();
    void log(const std::string& message);
    
    // inline 함수 (헤더에 정의 가능)
    inline void debug(const std::string& msg) {
        log("[DEBUG] " + msg);
    }
    
private:
    Logger() = default;
    
    // C++17 inline static
    inline static int logCount = 0;
};

#endif
// logger.cpp
#include "logger.h"

Logger& Logger::getInstance() {
    static Logger instance;
    return instance;
}

void Logger::log(const std::string& message) {
    std::cout << message << std::endl;
    ++logCount;
}
// main.cpp
#include "logger.h"

int main() {
    Logger& logger = Logger::getInstance();
    logger.log("Hello");
    logger.debug("Debug message");
    
    return 0;
}

7. ODR 위반 탐지

컴파일러 경고

# GCC
g++ -Wall -Wextra -Werror file1.cpp file2.cpp

# Clang
clang++ -Weverything file1.cpp file2.cpp

링커 에러

# 링크 에러 예시
/usr/bin/ld: file2.o: in function `func()':
file2.cpp:(.text+0x0): multiple definition of `func()'; 
file1.o:file1.cpp:(.text+0x0): first defined here

nm 도구

# 심볼 확인
nm file1.o
nm file2.o

# 중복 정의 찾기
nm *.o | grep " T " | sort

정리

핵심 요약

  1. ODR: 하나의 정의만 허용
  2. 변수: 전체 프로그램에서 하나
  3. 함수: 전체 프로그램에서 하나
  4. 클래스: 각 번역 단위에서 동일한 정의
  5. 예외: inline, 템플릿, constexpr
  6. C++17: inline 변수, inline static 멤버

ODR 준수 가이드

항목헤더 파일소스 파일
변수 선언extern int var;int var = 0;
함수 선언void func();void func() {}
클래스 정의class C {};-
inline 함수inline void f() {}-
템플릿template<T> class C {};-
inline 변수 (C++17)inline int var = 0;-

실전 팁

헤더 파일:

  • 선언만 포함 (정의는 소스 파일)
  • inline 함수는 헤더에 정의 가능
  • 템플릿은 헤더에 정의 필수
  • C++17 inline 변수 활용

링크 에러 방지:

  • 헤더 가드 사용 (#ifndef 또는 #pragma once)
  • 비인라인 함수는 소스 파일에 정의
  • extern으로 변수 선언/정의 분리
  • static 함수는 내부 링크 (각 파일마다 별도)

디버깅:

  • 링크 에러 메시지 확인 (multiple definition)
  • nm 도구로 심볼 중복 확인
  • 컴파일러 경고 활성화

다음 단계

  • C++ Extern Linkage
  • C++ Header Files
  • C++ Function Overloading

관련 글

  • C++ Compilation Process |
  • C++ multiple definition 에러 |
  • C++ 헤더 온리 라이브러리 |
  • C++ Linking |
  • C++ Name Mangling |