C++공부/C++ Templates

부록 A. 단정의 법칙(One Definition Rule)

아헿헿헿 2022. 7. 8. 00:28

ODR로 알려진 단정의 법칙은 잘 구성된 C++ 프로그램 구조의 주춧돌로, 인라인이 아닌 함수는 단 한번만 정의되고 클래스와 인라인 함수는 번역 단위 내에서 최대 한번만 정의될 수 있어, 같은 실체에 대한 모든 정의를 같게 합니다. 하지만 템플릿 인스턴스와 함께 세부적으로 들어가면 상당히 복잡해집니다.

A.1 번역 단위

C++ 프로그램을 작성할 때 파일들은 코드로 구성되는데, 파일의 경계보다는 번역 단위가 ODR에 있어 중요합니다. 번역 단위는 전처리자가 제 할일을 마친 후 컴파일러에게 넘긴 코드 덩어리를 일컫습니다. 전처리자는 컴파일 지시자와 주석을 수행하고, #include로 추가된 파일을 삽입하고 매크로를 확장합니다. 만약 두 번역 단위 사이에 외부 링크를 갖는 대응 선언을 갖는다면, 번역 단위 경계를 넘는 연결이 생깁니다.

A.2 선언과 정의

ODR에서 선언과 정의의 정확한 의미는 매우 중요합니다.

선언은 프로그램에 이름을 도입 혹은 재도입하는 C++ 생성 과정입니다. 선언은 어떤 실체를 어떻게 도입할 것인가에 따라 정의됩니다.

  • 네임스페이스와 네임스페이스 별칭 - 네임 스페이스와 그들의 별칭에 대한 선언은 항상 정의입니다. 네임 스페이스의 멤버들의 목록은 이후에 확장될 수 있기 떄문입니다.
  • 클래스, 클래스 템플릿, 함수, 함수 템플릿, 멤버 함수와 멤버 함수 템플릿 - 선언이 중괄호{}로 묶인 내용을 가진 이름으로 이뤄졌을 경우에만 정의입니다. (선언만 한 경우 X) 이는 공용체, 연산자 등의 템플릿 버전에 대한 명시적 특수화에도 적용됩니다.
  • 열거형 - 열거될 값들을 나열한 목록이 중괄호로 묶여 뒤따라올 때에만 선언은 정의와 동일합니다.
  • 지역 변수와 정적이지 않은 데이터 멤버 - 이들은 항상 정의로 취급되며, 그차이는 거의 문제가 되지 않습니다. 함수 정의 안에서 함수 파라미터를 선언하는 것 자체는 정의며 지역 변수를 나타내지만, 정의가 아닌 함수 선언 내에 있는 함수 파라미터는 정의가 아닙니다.
  • 전역 변수 - 선언이 extern 바로 뒤에 나오지 않았거나 초기화자를 갖고 있다면, 전역 변수의 선언은 해당 변수에 대한 정의이지만, 그렇지 않다면 정의가 아닙니다.
  • 정적 데이터 멤버 - 자신이 멤버로 속해있는 클래스 혹은 클래스 템플릿의 외부에서 선언된 경우나 클래스나 클래스 템플릿에서 inline이나 constexpr로 선언된 경우만 정의입니다.
  • 명시적이고 부분적인 특수화 - template<>이나 template<...>이 붙은 선언이 정의일 경우 해당 선언은 정의입니다. 정적 데이터 멤버의 명시적 특수화나 정적 데이터 멤버 템플릿은 초기화자가 포함될 경우에만 정의입니다.

그 외의 선언은 정의가 아니며, 형식 별칭, using 선언, using 지시자, 템플릿 파라미터 선언, 명시적 인스턴스화 지시자, static_assert 선언 등은 정의가 아닙니다.

A.3 상세한 단정의 법칙

A.3.1 프로그램당 한번

다음의 아이템들은 프로그램 당 단 한번만 정의돼야 합니다.

  • 인라인이 아닌 함수와 인라인이 아닌 멤버 함수
  • 인라인이 아닌 변수
  • 인라인이 아닌 정적 데이터 멤버

이는 다른 네임스페이스와 같이 내부 링크를 갖는 실체(이름 없는 네임스페이스 혹은 static 명시자가 존재하는 실체들)라면 적용되지 않습니다. 그러한 실체가 동일한 이름을 갖는다 하더라도 이들은 서로 다른 것으로 간주됩니다. 같은 맥락으로 이름 없는 네임스페이스에서 선언됐더라도 서로 다른 번역 단위에 나타난 실체라면 서로 다른 것으로 간주됩니다.

http://egloos.zum.com/sweeper/v/3082953 참조

만약 이번 절에서 살펴본 제약 조건을 어기더라도, C++ 컴파일러/링커가 진단 메세지를 출력하지 않고 정의 중첩 혹은 정의 없음을 보고합니다.

A.3.2 번역 단위별 한번 정의 제약

어떤 실체도 번역 단위 내에서 한 번 이상 정의될 수 없습니다. 이를 위해서 헤더 파일의 코드는 가드로 둘러쌉니다. ODR은 특정 실체가 특정 환경에서 정의돼야만 한다고 명시합니다. 이 법칙은 클래스형, 인라인 함수와 내보내기되지 않은 템플릿에서도 마찬가지로 정의됩니다.

 클래스형 X는 사용되기 전에 해당 번역 단위에서 다음과 같은 방식으로 정의되어야만 합니다.

  • X형의 객체 생성. 이때 X형의 객체를 포함하는 객체가 생성되는 것과 같은 간접적인 생성도 포함됩니다.
  • X형의 데이터 멤버 선언
  • X형 객체에 대한 sizeof나 typeid 사용
  • X형의 멤버에 명시적으로나 암묵적으로 접근
  • 표현식을 통해 X형으로 바꾸거나 X형에서 바뀌는 경우, 이에 해당하는 포인터나 참조자를 바꾸는 경우
  • X형의 객체에 값 할당
  • X형의 인자를 갖는 함수 혹은 X형을 반환하는 함수를 정의하거나 호출할 경우. 선언만 하는 경우는 해당X

형식에 대한 법칙은 클래스 템플릿에서 생성된 X형에도 동일하게 적용되므로, X형이 선언돼야만 하는 상황에서 대응되는 템플릿은 꼭 정의돼야만 합니다. 인라인 함수는 모든 번역 단위에서 정의돼야만 하지만, 이는 클래스형과는 달리 이들의 정의는 사용 지점 이후에 나타나도 괜찮습니다.

A.3.3 교차 번역 단위 동등 제약

한 번역 단위보다 더 많은 곳에서 특정 실체를 정의할 수 있게 되면 일치하지 않은 다중 정의가 생겨 오류가 발생할 수 있습니다. 교차 번역 단위 제약 조건에 따르면 실체가 두 군데에서 정의됐을 때 두 장소는 정확히 같은 토큰의 순열을 가져야 합니다. 더구나 이러한 토큰들은 서로의 문맥에서 동일한 의미를 가져야만 합니다.

static int counter = 0;
inline void increaseCounter() { ++counter; };

////
static int counter = 2;
inline void increaseCounter() { ++counter; };

다음 두 함수는 다른 counter 토큰을 사용하기에 오류가 발생합니다. 이러한 실체에 대한 정의를 헤더 파일을 두어 필요할 때마다 #include로 포함시키려 할 때는 거의 모든 상황에서 토큰 순서가 같도록 주의해야 합니다.

// 번역 단위 1
class X {
  public:
    X(int, int);
    X(int, int, int);
};

X::X(int, int = 0) {};

class D {
  X x = 0;
};

D d1; // X(int, int) called by D()


// 번역 단위 2
class X {
  public:
    X(int, int);
    X(int, int, int);
};

X::X(int, int = 0, int = 0) {};

class D {
  X x = 0;
};

D d2; // X(int, int, int) called by D()

동일한 토큰은 동일한 실체를 가리켜야 한다는 법칙에도 예외가 존재하는데, 동일한 토큰이 같은 값을 갖고 결과 표현식의 주소를 사용하지 않는 상수를 가르킨다면 두 토큰은 동일한 것으로 간주합니다.

 템플릿의 경우, 템플릿에서 이름은 두 단계 바인드됩니다. 종속되지 않은 이름은 템플릿이 정의되는 시점에 바인드되며, 이런 이름들의 경우 동등 법칙은 다른 비템플릿의 정의들과 유사하게 처리되지만, 인스턴스화 시점에 바인드되는 이름들은 그 지점에서 동등 법칙이 적용돼야만 하고, 바인딩은 동등해야만 합니다.