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
정리
핵심 요약
- ODR: 하나의 정의만 허용
- 변수: 전체 프로그램에서 하나
- 함수: 전체 프로그램에서 하나
- 클래스: 각 번역 단위에서 동일한 정의
- 예외:
inline, 템플릿,constexpr - 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 |