정의

정의 (Definitions)

정의(definition) 란, 선언(declaration)이 소개하는 엔티티를 완전히 규정하는 선언이에요. 모든 선언이 정의인 건 아니에요. 몇 가지 예외만 제외하면, 대부분의 선언은 동시에 정의예요. 이 페이지에서는 그 예외가 무엇인지, 그리고 C++에서 유명한 일치 정의 규칙(One Definition Rule, ODR) 이 정확히 무엇을 요구하는지를 다뤄요.

출처: cppreference

본문

정의가 아닌 선언들

정의가 아닌 선언에는 다음이 있어요.

  • 함수 본문이 없는 함수 선언.
int f(int); // f를 선언만 하고 정의하지는 않음
  • 이니셜라이저가 없는 extern 저장 클래스 지정자 또는 언어 결합(language linkage) 지정자(예: extern "C")를 가진 선언.
extern const int a;     // a를 선언만 하고 정의하지는 않음
extern const int b = 1; // b를 정의함
  • 클래스 정의 안의 비인라인(C++17부터) 정적 데이터 멤버 선언.
struct S
{
    int n;               // S::n을 정의
    static int i;        // S::i를 선언만 하고 정의하지는 않음
    inline static int x; // S::x를 정의함
};                       // S를 정의

int S::i;                // S::i를 정의
  • (C++17부터, 폐기 예정) 클래스 안에서 constexpr 지정자로 정의된 정적 데이터 멤버의 네임스페이스 스코프 선언. struct S { static constexpr int x = 42; }에서 x는 암시적으로 inline이라 S::x를 정의해요. 그래서 constexpr int S::x;S::x를 선언만 하고 재정의는 아니에요.

  • 클래스 이름 선언 (전방 선언이거나 다른 선언에서 정교한 타입 지정자(elaborated type specifier)로 쓴 경우).

struct S;             // S를 선언만 하고 정의하지는 않음

class Y f(class T p); // Y와 T(그리고 f와 p)를 선언만 함
  • (C++11부터) 열거형의 불투명 선언(opaque declaration): enum Color : int;는 Color를 선언만 하고 정의하지 않아요.

  • 템플릿 매개변수 선언.

template<typename T> // T를 선언만 하고 정의하지는 않음
  • 정의가 아닌 함수 선언 안의 매개변수 선언.
int f(int x); // f와 x를 선언만 하고 정의하지는 않음

int f(int x)  // f와 x를 정의함
{
    return x + a;
}
  • typedef 선언.
typedef S S2; // S2를 선언만 하고 정의하지는 않음 (S는 불완전해도 됨)
  • (C++11부터) 별칭 선언(alias-declaration): using S2 = S;는 S2를 선언만 하고 정의하지 않아요 (S는 불완전해도 됨).

  • using 선언.

using N::d; // d를 선언만 하고 정의하지는 않음
  • (C++17부터) 연역 가이드(deduction guide) 선언 — 어떤 엔티티도 정의하지 않아요.
  • (C++11부터) static_assert 선언, 속성(attribute) 선언 — 어떤 엔티티도 정의하지 않아요.
  • 빈 선언(empty declaration) — 어떤 엔티티도 정의하지 않아요.
  • using 지시문(using-directive) — 어떤 엔티티도 정의하지 않아요.
  • (C++11부터) 명시적 인스턴스화 선언("extern template"): extern template f<int, char>;f<int, char>를 선언만 하고 정의하지 않아요.
  • 정의가 아닌 선언인 명시적 특수화(explicit specialization).
template<>
struct A<int>; // A<int>를 선언만 하고 정의하지는 않음

asm 선언은 어떤 엔티티도 정의하지 않지만, 정의로 분류돼요.

필요하면 컴파일러가 기본 생성자, 복사 생성자, 이동 생성자, 복사 대입 연산자, 이동 대입 연산자, 소멸자를 암시적으로 정의할 수 있어요.

어떤 객체의 정의가 불완전 타입 또는 추상 클래스 타입의 객체를 만들게 되면, 그 프로그램은 ill-formed예요.

일치 정의 규칙 (One Definition Rule)

어떤 변수·함수·클래스 타입·열거형 타입·개념(C++20부터)·템플릿의 정의는 한 번역 단위 안에 단 하나만 허용돼요 (이 중 일부는 여러 선언을 가질 수 있지만, 정의는 하나뿐이어야 해요).

odr-사용(odr-used) 되는 모든 비인라인 함수나 변수프로그램 전체(표준·사용자 라이브러리 포함)에 정확히 하나의 정의가 있어야 해요. 컴파일러는 이 위반을 진단할 의무가 없지만, 위반한 프로그램의 동작은 정의되지 않음(undefined) 이에요.

(C++17부터) 인라인 함수나 인라인 변수는 그것이 odr-사용되는 모든 번역 단위에 정의가 필요해요.

클래스는 그것을 완전체(complete) 로 요구하는 방식으로 사용되는 곳마다 정의가 필요해요.

다음 각각은 프로그램 안에 정의가 둘 이상 있어도 돼요: 클래스 타입, 열거형 타입, 인라인 함수(C++17부터 인라인 변수), 템플릿화된 엔티티(템플릿 또는 템플릿의 멤버. 단 완전한 템플릿 특수화는 아님). 단 다음 조건을 모두 만족해야 해요.

  • 각 정의가 서로 다른 번역 단위에 나타난다.
  • (C++20부터) 정의들이 이름 붙은 모듈(named module)에 붙어 있지 않다.
  • 각 정의가 같은 토큰 열로 이루어진다 (보통 같은 헤더에 나타남).
  • 각 정의 안에서의 이름 검색이 (오버로드 해석 후) 같은 엔티티를 찾는다. 단 다음 예외가 있다.
    • 내부 또는 결합 없음(no linkage) 결합을 가진 상수는, odr-사용되지 않고 모든 정의에서 같은 값을 가지는 한, 서로 다른 객체를 가리켜도 된다.
    • (C++11부터) 기본 인자나 기본 템플릿 인자에 있지 않은(C++20부터) 람다 표현식은, 그것을 정의하는 토큰 열에 의해 유일하게 식별된다.
    • 변환·할당·할당 해제 함수를 포함한 오버로드된 연산자는 각 정의로부터 같은 함수를 가리킨다 (정의 안에서 정의된 것을 가리키는 경우는 제외).
    • 대응하는 엔티티가 각 정의에서 같은 언어 결합을 가진다 (예: include 파일이 extern "C" 블록 안에 있지 않다).
    • 어떤 정의에서 const 객체가 상수 초기화(constant-initialized)되면, 각 정의에서도 상수 초기화된다.
    • 위 규칙들은 각 정의에서 쓰이는 모든 기본 인자에도 적용된다.
  • 클래스에 암시적으로 선언된 생성자가 있으면, 그것이 odr-사용되는 모든 번역 단위에서 기반과 멤버에 대해 같은 생성자를 호출해야 한다.
  • (C++20부터) 기본 삼방 비교(defaulted three-way comparison)가 있는 클래스면, 그것이 odr-사용되는 모든 번역 단위에서 기반과 멤버에 대해 같은 비교 연산자를 호출해야 한다.
  • 정의가 템플릿에 관한 것이면, 이 모든 요구사항은 정의 지점의 이름과 인스턴스화 지점의 종속 이름 모두에 적용된다.

이 모든 요구사항을 만족하면, 프로그램은 프로그램 전체에 정의가 하나뿐인 것처럼 동작해요. 그렇지 않으면 프로그램은 ill-formed이며 진단이 필요 없어요.

참고로 C에서는 타입에 대한 프로그램 전체 ODR이 없어요. 그래서 같은 변수에 대한 extern 선언조차 호환되기만 하면 서로 다른 타입을 가질 수 있어요. 반면 **C++**에서는 같은 타입을 선언할 때 쓰는 소스 코드 토큰이 위에서 설명한 대로 같아야 해요. 한 .cpp 파일이 struct S { int x; };를 정의하고 다른 .cpp 파일이 struct S { int y; };를 정의해서 둘을 링크하면 동작이 정의되지 않아요. 보통 익명 네임스페이스(unnamed namespace) 로 이것을 해결해요.

엔티티의 이름 지음 (Naming an entity)

변수는, 표현식이 그것을 가리키는 식별자 표현식(identifier expression)이라면, 그 표현식에 의해 이름 지어진다고 해요.

함수는 다음 경우에 표현식이나 변환에 의해 이름 지어져요.

  • 이름이 표현식이나 변환으로 나타나는 함수(이름 붙은 함수, 오버로드된 연산자, 사용자 정의 변환, 사용자 정의 배치 형태의 operator new, 기본이 아닌 초기화 포함)는, 오버로드 해석이 그것을 선택하면 그 표현식에 의해 이름 지어져요. 단 미한정 순수 가상 멤버 함수나 순수 가상 함수에 대한 멤버 포인터는 예외예요.
  • 클래스에 대한 할당·할당 해제 함수는, 표현식 안에 나타난 new 표현식에 의해 이름 지어져요.
  • 클래스에 대한 할당 해제 함수는, 표현식 안에 나타난 delete 표현식에 의해 이름 지어져요.
  • 객체를 복사하거나 이동하도록 선택된 생성자는, 복사 생략(copy elision)이 일어나도 그 표현식이나 변환에 의해 이름 지어진다고 간주돼요. 어떤 문맥에서 prvalue를 쓰는 것은 객체를 복사·이동하지 않아요. (C++17부터, 강제 생략 참고)

잠재적으로 평가되는(potentially evaluated) 표현식 또는 변환은, 함수를 이름 지으면 그것을 odr-사용해요. (C++11부터) 어떤 잠재적으로 상수 평가되는 표현식이나 변환이 constexpr 함수를 이름 지으면, 평가가 일어나지 않아도 그 함수는 상수 평가에 필요해지고, 이는 defaulted 함수의 정의나 함수 템플릿 특수화의 인스턴스화를 촉발해요.

잠재 결과 (Potential results)

표현식 E잠재 결과(potential results) 집합E 안에 나타나는 식별자 표현식들의 (빈 집합일 수 있는) 집합으로, 다음과 같이 합쳐져요.

  • E가 식별자 표현식이면, 표현식 E 자신이 유일한 잠재 결과예요.
  • E가 첨자 표현식(E1[E2])인데 한 피연산자가 배열이면, 그 피연산자의 잠재 결과가 집합에 포함돼요.
  • EE1.E2 또는 E1.template E2 형태의 클래스 멤버 접근 표현식이고 비정적 데이터 멤버를 가리키면, E1의 잠재 결과가 집합에 포함돼요.
  • E가 정적 데이터 멤버를 가리키는 클래스 멤버 접근 표현식이면, 그 데이터 멤버를 가리키는 식별자 표현식이 집합에 포함돼요.
  • EE1.*E2 또는 E1.*template E2 형태의 멤버 포인터 접근 표현식인데 두 번째 피연산자가 상수 표현식이면, E1의 잠재 결과가 집합에 포함돼요.
  • E가 괄호 안의 표현식((E1))이면, E1의 잠재 결과가 집합에 포함돼요.
  • E가 glvalue 조건 표현식(E1 ? E2 : E3, E2와 E3가 glvalue)이면, E2E3의 잠재 결과의 합집합이 집합에 포함돼요.
  • E가 콤마 표현식(E1, E2)이면, E2의 잠재 결과가 집합에 포함돼요.
  • 그 외에는 집합이 비어 있어요.

ODR-사용 (비공식 정의)

직관적으로 ODR-사용을 정리하면 다음과 같아요.

  • 객체는, 그 값이 (컴파일 타임 상수가 아니라면) 읽히거나, 쓰이거나, 주소가 취해지거나, 참조가 바인딩되면 odr-사용돼요.
  • 참조는, 사용되고 그 referent(가리키는 대상)가 컴파일 타임에 알려지지 않으면 odr-사용돼요.
  • 함수는, 그것에 대한 함수 호출이 이루어지거나 주소가 취해지면 odr-사용돼요.

엔티티가 odr-사용되면, 그 정의가 프로그램 어딘가에 존재해야 해요. 위반은 보통 링크 타임 오류로 나타나요.

struct S
{
    static const int x = 0; // 정적 데이터 멤버
    // odr-사용되면 클래스 밖의 정의가 필요
};

const int& f(const int& r);

int n = b ? (1, S::x) // 여기서 S::x는 odr-사용되지 않음
          : f(S::x);  // 여기서 S::x는 odr-사용됨: 정의가 필요

ODR-사용 (형식 정의)

지점 P에 나타나는 잠재적으로 평가되는 표현식 expr 에 의해 이름 지어진 변수 x는, 다음 조건 중 하나라도 만족하지 않는 한 expr에 의해 odr-사용돼요.

  • x가 P에서 상수 표현식에 쓸 수 있는 참조이다.
  • x가 참조가 아니고 (C++26 이전) expr이 어떤 표현식 E의 잠재 결과 집합의 원소이고, 다음 중 하나가 만족한다.
    • E가 폐기-값(discarded-value) 표현식이고, lvalue-to-rvalue 변환이 적용되지 않는다.
    • x가 P에서 상수 표현식에 쓸 수 있고 가변(mutable) 부분 객체가 없는 비휘발성(C++26부터) 객체이고, 다음 중 하나가 만족한다.
      • (C++26부터) E가 참조 타입의 비정적 데이터 멤버를 가리키고 객체 표현식이 비휘발성 한정 타입인 클래스 멤버 접근 표현식이다.
      • E가 비휘발성 한정 비클래스 타입이고, lvalue-to-rvalue 변환이 적용된다.
struct S { static const int x = 1; }; // S::x에 lvalue-to-rvalue 변환을
                                      // 적용하면 상수 표현식이 됨

int f()
{
    S::x;        // 폐기-값 표현식은 S::x를 odr-사용하지 않음

    return S::x; // lvalue-to-rvalue 변환이 적용되는 표현식은
                 // S::x를 odr-사용하지 않음
}
  • this잠재적으로 평가되는 표현식으로 나타나면(비정적 멤버 함수 호출 표현식의 암시적 this 포함) odr-사용돼요.
  • (C++17부터) 구조적 바인딩(structured binding)잠재적으로 평가되는 표현식으로 나타나면 odr-사용돼요.

함수는 다음 경우에 odr-사용돼요.

  • 잠재적으로 평가되는 표현식이나 변환에 의해 (아래처럼) 이름 지어지면.
  • 가상 멤버 함수는 (순수 가상 멤버 함수가 아니면) odr-사용돼요. 가상 멤버 함수의 주소는 vtable을 만드는 데 필요하기 때문이에요.
  • 클래스에 대한 비배치(non-placement) 할당·할당 해제 함수는 그 클래스의 생성자 정의에 의해 odr-사용돼요.
  • 클래스에 대한 비배치 할당 해제 함수는 그 클래스의 소멸자 정의에 의해, 또는 가상 소멸자의 정의 지점에서의 검색이 그것을 선택함으로써 odr-사용돼요.
  • 클래스 T대입 연산자가 다른 클래스 U의 멤버나 기반이라면, U의 암시적으로 정의된 복사 대입·이동 대입 함수에 의해 odr-사용돼요.
  • 클래스의 생성자(기본 생성자 포함)는 그것을 선택하는 초기화에 의해 odr-사용돼요.
  • 클래스의 소멸자는 그것이 잠재적으로 호출되면(potentially invoked) odr-사용돼요.

결함 보고 (Defect reports)

다음의 동작을 바꾸는 결함 보고가 이전에 공표된 C++ 표준에 소급 적용됐어요.

DR 적용 대상 공표된 동작 올바른 동작
CWG 261 C++98 프로그램에 관련 new/delete 표현식이 없어도 다형성 클래스의 할당 해제 함수가 odr-사용될 수 있었음 생성자와 소멸자를 포함하도록 odr-사용 경우 보완
CWG 678 C++98 엔티티가 서로 다른 언어 결합으로 정의될 수 있었음 이 경우 동작이 정의되지 않음
CWG 1472 C++98 상수 표현식에 나타나는 요구를 만족하는 참조 변수가, lvalue-to-rvalue 변환이 즉시 적용돼도 odr-사용됐음 이 경우 odr-사용되지 않음
CWG 1614 C++98 순수 가상 함수의 주소를 취하는 것이 그것을 odr-사용시켰음 함수는 odr-사용되지 않음
CWG 1741 C++98 잠재적으로 평가되는 표현식에서 즉시 lvalue-to-rvalue 변환되는 const 객체가 odr-사용됐음 odr-사용되지 않음
CWG 1926 C++98 배열 첨자 표현식이 잠재 결과를 전파하지 않았음 전파함
CWG 2242 C++98 정의 중 일부에서만 상수 초기화되는 const 객체가 ODR을 위반하는지 불명확했음 ODR을 위반하지 않음; 이 경우 객체는 상수 초기화됨
CWG 2300 C++11 서로 다른 번역 단위의 람다 표현식은 결코 같은 클로저 타입을 가질 수 없었음 일치 정의 규칙에 따라 같은 클로저 타입 가능
CWG 2353 C++98 정적 데이터 멤버가 그것에 접근하는 멤버 접근 표현식의 잠재 결과가 아니었음 그것이 잠재 결과임
CWG 2433 C++14 변수 템플릿이 프로그램 안에 여러 정의를 가질 수 없었음 가질 수 있음

참고 문헌 (References)

  • C++23 표준 (ISO/IEC 14882:2024): 6.3 One definition rule [basic.def.odr]
  • C++20 표준 (ISO/IEC 14882:2020): 6.3 One definition rule [basic.def.odr]
  • C++17 표준 (ISO/IEC 14882:2017): 6.2 One definition rule [basic.def.odr]
  • C++14 표준 (ISO/IEC 14882:2014): 3.2 One definition rule [basic.def.odr]
  • C++11 표준 (ISO/IEC 14882:2011): 3.2 One definition rule [basic.def.odr]
  • C++03 표준 (ISO/IEC 14882:2003): 3.2 One definition rule [basic.def.odr]
  • C++98 표준 (ISO/IEC 14882:1998): 3.2 One definition rule [basic.def.odr]

더 알아보기 (Learn more)

  • 선언 (Declarations) — 선언의 문법과 종류 전반. 정의가 아닌 선언들이 어떻게 쓰이는지의 배경. (cppreference)
  • 객체 (Object) — 저장 공간과 객체 생애주기의 기본 개념. 정의가 만들어내는 대상. (cppreference)
  • 링크 (Linkage) — 이름이 여러 번역 단위에 걸쳐 어떻게 결합되는지. ODR을 이해하는 데 꼭 필요한 배경. (cppreference)\