나만의 작은 도서관

[C++][Build] 컴파일 단계 - 번역 단위(translation unit)와 선언과 정의, 그리고 ODR 본문

C++/문법 및 메소드(STL)

[C++][Build] 컴파일 단계 - 번역 단위(translation unit)와 선언과 정의, 그리고 ODR

pledge24 2025. 4. 14. 17:23

ODR 예제. 출처: https://stackoverflow.com/questions/46988095/why-one-definition-rule-not-one-declaration-rule

개요

컴파일 단계는 프로그래밍 언어로 된 코드를 기계어로 변환하는 과정이다. 이 과정을 수행하는 컴파일러는 ODR(One Definition Rule, 유일 정의 규칙)이라는 정해진 규칙을 준수하는데, 이러한 ODR을 이해하기 위해 필요한 개념이 바로 번역 단위와 선언과 정의이다.

이 글에서는 컴파일 과정에서 준수하는 ODR이라는 규칙을 이해하기 위해 번역 단위가 무엇인지, 선언과 정의가 어떻게 구분되는지, 그리고 마지막으로 ODR 정의에 대한 해석을 간단하게 다룬다.


번역 단위(translation unit)

  • 번역 단위(이하 TU)란 하나의 소스 파일(.cpp)이 전처리 단계를 거친 후에 생긴 결과물을 의미하며, 컴파일러가 소스 파일(.cpp)을 처리하는 단위이다.
    • 즉, TU는 #include, #define과 같은 전처리기 지시문이 처리되어 모든 헤더파일과 매크로가 펼쳐진 순수한 C/C++ 코드 덩어리이다.
  • 각 TU는 컴파일러에게 전달되어 오브젝트 파일로 컴파일되고, 오브젝트 파일은 링커에게 전달되어 링킹과정을 통해 여러 오브젝트 파일(.o)을 하나의 실행 파일(.exe)로 결합한다.

TU 예제

  • 아래 코드에서 main.cpp는 #include "foo.h"가 전처리되고, foo.cpp는 #include "foo.h"와 #include <iostream>가 전처리된다.
  • 전처리는 각 소스파일에 대해 독립적으로 처리되며, 처리 후 각각 하나의 TU가 된다.
  • 이후 각 TU들은 컴파일러에 의해 오브젝트 파일로 컴파일된다.
// main.cpp
#include "foo.h"
int main() {
    foo();
}

// foo.h
void foo();

// foo.cpp
#include "foo.h"
#include <iostream>
void foo() {
    std::cout << "Hello\\n";
}

 

 


선언(declaration)과 정의(definition)

  • C++에서 선언과 정의는 엄격히 구분한다.
  • 선언(declaration)이란 TU에 새로운 식별자를 추가하거나, 기존의 선언된 식별자를 재선언하는 것을 의미한다.
    • 즉, C++에서 선언은 우리가 흔히 말하는 “XXX을 선언했다”의 선언과 의미가 같다.
  • 정의(definition)선언을 포함하는 개념으로, 선언된 개체(entity)를 완전히 정의함을 뜻한다.
    • 이렇게만 보면 선언과 정의의 차이점이 굉장히 헷갈리는데, 개인적으로 “구현부(중괄호 몸통)가 있으면 정의, 없으면 선언”이라고 이해했다.
  • 선언과 정의의 차이를 정리하자면 아래표와 같다.
구분 설명
선언 (Declaration) "이런 게 있을 거야"라고 알려주는 것. 크기나 위치를 알 필요는 없음.
정의 (Definition) 실제로 공간을 할당하거나 구현을 제공하는 것. 링커가 찾는 대상.

 

 

선언과 정의가 동시에 일어나는 유형들

  • 대표적으로 기본 타입의 일반 변수가 있다. 기본 타입의 일반 변수는 전역 변수이든, 멤버 변수이든, 초기화를 하든 안 하든 항상 모든 경우에서 선언과 정의가 동시에 일어난다.
    • 여기서 기본 타입의 일반 변수는 아무 키워드(static이나 inline)도 붙지 않은 기본 타입 변수를 의미한다.
int a; // 대입이 없지만, 선언과 정의가 동시에 일어난다.

 

 

선언과 정의가 분리되는 유형들

  • 식별자가 존재하지만 내부 구현이 되어있지 않은 경우를 “선언되었지만 정의되지 않았다”라고 한다. 이런 경우 컴파일 단계에서 오류가 발생하지 않지만, 링킹 단계에서 구현부를 찾지 못해 링크 오류가 발생하게 된다.
  • “선언되었지만 정의되지 않은” 경우가 발생하려면 해당 식별자의 유형이 선언과 정의가 분리될 수 있어야 한다. C++에서는 선언과 정의가 분리되는 유형이 다양한데, 해당 유형들은 다음과 같다.
// 선언과 정의과 분리되는 유형 예제

// 1) 함수(템플릿 함수의 경우 분리가 되지만 같은 공간에 정의도 존재해야한다.)
int add(int a, int b); // 함수 선언.
int add(int a, int b) {return a + b;} // 함수 정의

// 2) 구조체 또는 클래스
struct myStruct; // 구조체 선언(전방 선언 취급)
struct myStruct {}; // 구조체 정의
class myClass; // 클래스 선언(전방 선언 취급)
class myClass {}; // 클래스 정의

// 3) 멤버 함수
class myClass2{
public:
    void doSomething(); // 멤버 함수 선언
}
myClass2::doSomething(){}; // 멤버 함수 정의

// 4) 열거형
enum Color; // 열거형 선언
enum Color { RED, GREEN, BLUE }; // 열거형 정의
    
// 5) extern 전역 변수
extern int g_value; // extern 전역 변수는 선언만 한다.
  • static 멤버 변수의 경우 클래스 내부에서 선언이, 클래스 외부에서 정의가 되기 때문에 선언과 정의가 분리되지만, inline static(C++17) 멤버 변수는 클래스 내부에서 정의되기 때문에 분리되지 않는다.
struct S{
    int n; // S::n의 정의
    static int i; // S::i를 선언, But 정의는 아님
    inline static int x; // S::x를 정의
}; // S를 정의
int S::i; // S::i를 정의

 

 


유일 정의 규칙(One Definition Rule, ODR)

  • 유일 정의 규칙이란 “각 TU에 존재하는 모든 변수, 함수, 클래스, enum, 템플릿 등등의 정의는 유일해야 하며, inline이 아닌 모든 함수나 변수들의 정의는 전체 프로그램에서 유일해야 한다”는 규칙이다.

문장 해석

  1. “각 TU에 존재하는 모든 변수, 함수, 클래스, enum, 템플릿 등등의 정의는 유일해야 하며,”
    • 문장 그대로, 같은 TU안에 여러 번 정의하면 안 된다는 의미이다. 즉, 아래와 같은 코드는 ODR을 위반한다.
    int f() {  // 함수 f의 첫번째 정의
      return 0;
    }
    
    int f() {  // 함수 f의 두번째 정의 <-- ODR 위반
      return 0;
    }
    
    int f();
    • 하지만 "선언이 유일해야 한다"는 내용을 없었으므로, 같은 시그니처를 가진 함수를 여러 번 선언해도 ODR을 위반하지 않는다.
    • 따라서, 아래와 같은 코드를 같은 소스 파일에 작성해도 문제가 되지 않는다.
    int f();
    int f();
    int f();
  2. “inline이 아닌 모든 함수나 변수들의 정의는 프로그램 전체에서 유일해야 한다.”
    • 위 문장에서 알 수 있듯 inline 함수나 변수는 ODR의 예외 대상이기 때문에, inline 키워드를 붙이면 여러 번 정의하는 것이 가능하다.
    • 하지만 그렇다고 inline 함수가 서로 다른 정의를 가질 수 있는 것은 아니다. inline 함수는 그저 여러 번 정의할 수 있을 뿐이다.
      • 만약 inline 함수를 여러 번 정의한 경우, 오류가 발생하진 않지만 링커가 이 중 하나를 선택해 사용하므로, 의도치 않은 inline 함수 정의가 각 TU에서 사용될 수 있다.(이는 굉장히 좋지 못한 결과이다.)
// file1.cpp
inline int func() { return 1; } // OK
// inline int func() { return 2; } // 재정의는 안됨.

// file2.cpp
inline int func() { return 1; } // OK 

 


참고 자료

https://modoocode.com/320