14.1 주문형 인스턴스화
C++ 컴파일러가 템플릿 특수화를 만나게 되면 컴파일러는 템플릿 파라미터 자리에 제공된 인자들을 갖고 해당 특수화를 생성합니다. 이를 암묵적 인스턴스화 혹은 자동 인스턴스화라고 부릅니다.
일반적인 클래스의 경우 형식에 대한 포인터나 참조자 선언은 클래스 템플릿의 선언만 필요하며 정의는 불필요하다. 메모리 공간 할당을 위한 메모리 크기를 알아야 하거나 특수화의 멤버에 접근하고자 한다면, 영역 내에 전체 템플릿 정의가 존재해야합니다. 특히, 메모리 공간을 할당해줄 때에는 메모리 크기만이 아닌 new나 delete라는 연산자를 멤버 연산자로 선언하지 않았는지, 접근 가능한 기본 생성자를 갖고 있는지를 결정하기 위하여 인스턴스화를 수행해야만 합니다.
클래스 템플릿의 멤버에 대한 접근 필요성이 항상 소스코드에 명시적으로 표시되지는 않을 때도 존재합니다. 예를 들어 C++ 오버로딩 해석에서는 후보 함수들의 파라미터에 대한 클래스형을 명시적으로 알아야 합니다.
template<typename T>
class C {
public:
C(int);
};
void candidate(C<double>); // --- (1)
void candidate(int); // --- (2)
int main() {
candidate(42);
};
위의 예제에서 main문 안의 candidate에 대해 1, 2번 모두 수행 가능합니다. 이때 2번의 함수를 호출하도록 만들어지겠지만, 컴파일러에서는 1번 지점에서의 선언도 호출에 대한 사용 후보인지를 알아보기 위해 인스턴스화되며 인스턴스화가 만약 필요 없다면 그렇게 처리합니다.
14.2 게으른 인스턴스화
기본적으로 클래스형은 완전해야하기 때문에 컴파일러가 템플릿 정의에서 완전한 정의를 생성하지만, 컴파일러가 인스턴스화할 때 게으르게 수행하여 모든 것을 인스턴스화 시키지 않을 수도 있다.
부분과 전체 인스턴스화
template<typename T>
T f (T p) { return 2 * p };
decltype(f(2)) x = 2;
이 예제의 경우 형식으로서 사용하기 때문에 템플릿 f()를 완전히 인스턴스화할 필요는 없고, 선언만 치환하도록 하며 이를 부분 인스턴스화라고 합니다. 또한 이처럼 클래스 템플릿의 인스턴스화가 완전한 형식일 필요가 없는 인스턴스를 가리킨다면 해당 클래스 템플릿의 인스턴스에 대한 전체 인스턴스화를 수행할 필요가 없습니다.
template<typename T>
class Q { using Type = typename T::Type };
Q<int>* p = 0; // OK : Q<int>의 몸체는 치환되지 않는다.
다음의 경우 T가 int일 때 T::Type은 말이 안되기 때문에 Q<int>를 전체 인스턴스화하면 오류가 발생하지만, Q<int>는 완전할 필요가 없기 때문에 전체 인스턴스화가 일어나지 않고 코드도 무사합니다.
인스턴스화된 컴포넌트
클래스 템플릿이 암묵적으로 인스턴스화될 때 멤버의 각 선언도 인스턴스화되지만 그에 해당하는 정의는 인스턴스화되지 않습니다. 먼저 클래스 템플릿에 익명 공용체가 존재한다면, 공용체의 정의에 속하는 멤버들도 인스턴스화 됩니다. 가상 멤버 함수의 경우에는 가상 호출 메커니즘을 위한 내부 구조를 만들기 위해서 가상 함수가 링크될 수 있는 실체이어야 하기에 템플릿 인스턴스화의 결과에 따라 인스턴스화 여부가 정해집니다.
템플릿을 인스턴스화할 때 기본 함수 호출 인자가 따로 고려되는데, 기본 인자는 실제로 함수에서 사용되지 않는 한 인스턴스화되지 않습니다. 또한, 필요로 되지 않는 예외 명세와 기본 멤버 초기화자는 인스턴스화 되지 않습니다.
template<typename T>
class Safe {
};
template<int N>
class Danger {
int arr[N]; // OK here, although would fail for N<=0
};
template<typename T, int N>
class Tricky {
public:
void noBodyHere(Safe<T> = 3); // 기본값을 사용하는데 문제가 없는 동안은 OK
void inclass() {
Danger<N> noBoomYet; // inclass()가 N<=0로 사용되는 동안은 OK
}
struct Nested {
Danger<N> pfew; // Nested가 N<=0로 사용되는 동안은 OK
};
union { // due anonymous union:
Danger<N> anonymous; // Tricky가 N<=0로 인스턴스화되는 동안은 OK
int align;
};
void unsafe(T (*p)[N]); // Tricky가 N<=0로 인스턴스화되는 동안은 OK
void error() {
Danger<-1> boom; // \IBalways ERROR (which not all compilers detect)
}
};
위의 경우 N은 0이나 음수일 수도 있지만, 컴파일러는 유효한 파라미터가 들어올 것이라는 가정 하에 음수로 인스턴스화되기 전까지는 아무런 문제도 발생하지 않습니다. error()의 경우처럼 음수임을 명시하며 이에 대한 정의가 필요한 경우 에러가 발생합니다.
14.3 C++ 인스턴스화 모델
템플릿 인스턴스화는 해당 템플릿 실체에서 템플릿 파라미터를 적절히 치환해 일반 형식, 함수나 변수를 만드는 과정입니다. 이를 위해 많은 세부 사항이 정립되어야만 합니다
두 단계 룩업
템플릿을 파싱하는 것이 첫 번째 단계이고, 인스턴스화하는 것이 두 번째 단계입니다.
- 첫 번째 단계에서 템플릿이 일반 룩업 법칙과 인자 종속 룩업(ADL) 법칙에 따라 파싱되는 동안 종속되지 않은 이름도 룩업됩니다. 한정되지 않은 종속 이름 역시 같은 방식으로 룩업되지만 이 룩업의 결과는 템플릿이 인스턴스화돼 추가적인 룩업이 수행될 때까지 끝나지 않습니다.
- 두번째 단계에서 한정된 종속 이름이 룩업되며, 한정되지 않은 종속 이름을 위해 추가로 ADL을 수행합니다.
한정되지 않은 종속 이름을 위해 초기 일반 룩업을 사용해 이름이 템플릿인지 아닌지 결정합니다.
namespace N {
template<typename> void g() {}
enum E { e };
}
template<typename> void f() {}
template<typename T> void h(T P) {
f<int>(p); // #1
g<int>(p); // #2
}
int main() {
h(N::e);
};
#1 행에서 f의 선언이 템플릿임을 알기에 <를 꺾쇠의 시작으로 인식하지만 #2 행에서는 g라는 템플릿이 namespace N에 가려져 보이지 않기 때문에 템플릿으로 인식하지 않고서 <를 비교 연산으로 생각하기에 오류가 발생합니다. 만약 여기서 통과가 됐다면 T = N::E를 통해 N::g라는 템플릿을 찾을 수 있었을 것이다.
인스턴스화 시점
인스턴스화 지점(POI, Point Of Instantiation)은 코드가 템플릿 특수화를 참조할 때 그에 맞는 특수화를 생성하게 해당 템플릿의 정의가 특수화를 생성하기 위해 인스턴스화돼야 한다는 것을 알리기 위해 생성됩니다. POI는 치환된 템플릿이 삽입될 소스코드 위치를 말합니다.
class MyInt {
public:
MyInt(int i);
};
MyInt operator-(MyInt const&);
bool operator>(MyInt const&, MyInt const&);
using Int = MyInt;
template<typename T>
void f(T i)
{
if (i > 0) {
g(-i);
}
}
// #1
void g(Int)
{
// #2
f<Int>(42); // 호출 지점
// #3
}
// #4
f<Int>42에서 템플릿 f에서 T를 MyInt로 치환하여 인스턴스화해야 하는 것을 알기에 POI가 생성됩니다. 이때 #2와 #3의 지점에는 C++ 컴파일러가 정의를 이곳에 삽입하는 것을 허용하지 않습니다. #1 지점이 POI라면 g(Int)가 보이지 않아 g(-i)를 해석할 수 없어, #4의 위치에 POI가 생성됩니다.
이 예제의 특별한 점은 int 대신 MyInt를 사용했다는 점인데, 이는 POI가 수행되는 두 번째 룩업은 오로지 ADL로만 이루어지기 때문에, int와 같은 built-in의 경우는 연관된 네임스페이스가 존재하지 않아 POI 룩업이 일어나지 않아 g 함수를 찾지 못하게 됩니다. 반면에 MyInt는 선언된 네임스페이스가 연관되어 같은 네임스페이스에서 선언된 함수를 찾아보게 되므로, 찾을 수 있게 됩니다.
template<typename T>
class S{
public:
T m;
};
// #1
unsigned long h()
{
return (unsigned long)sizeof(S<int>);
}
// #2
다음의 경우는 위와 다르게 sizeof가 쓰이면서 S<int>의 크기가 알려져야지만 sizeof(S<int>) 표현식이 유효하기 때문에 #2에 POI가 위치하는 것이 아닌 #1에 위치하게 됩니다.
또한 템플릿 안에 템플릿 변수가 존재하는 경우는 내부에 존재하는 템플릿의 인스턴스화가 외부보다 먼저 이루어지게 됩니다. 일반적으로 한 번역 단위 내에서도 같은 인스턴스에 대해 여러 POI를 가질 수 있는데, 클래스의 경우는 첫 번째 POI만 유지되고 나머지는 무시됩니다. 클래스가 아닌 경우는 모든 POI가 유지되어야 합니다.
많은 컴파일러가 번역 단위의 끝에 이르기 전까지는 인라인이 아닌 함수 템플릿에 대해 실제 인스턴스화를 하지 않지만, 인스턴스화를 사용해 연역된 반환형을 결정하거나 함수가 constexpr이며 이에 대한 평가를 해야하는 경우에는 지연 불가능합니다.
14.4 구현 방식
모든 구현은 컴파일러와 링커라는 고전적인 구성 요소를 사용합니다. 컴파일러는 소스코드를 기호 주석이 달린 기계어 코드를 갖는 오브젝트 파일로 번역하고, 링커는 오브젝트 파일들을 결합하고 그들이 가진 기호적 상호 참조를 해석해 실행 가능한 프로그램이나 라이브러리를 생성합니다.
번역 단위에서 템플릿 특수화가 사용된다면 컴파일러는 모든 번역 단위에 대해 인스턴스화를 반복하여, 한 클래스 템플릿에 대한 다중 인스턴스화는 클래스 정의의 다중 포함이 됩니다. 이는 몇가지 문제가 발생할 수 있지만, 구현 내부에서 다양한 표현식과 선언들을 검증하고 해석될 때만 사용됩니다.
하지만 함수 템플릿을 인스턴스화하게 되어 함수에 대한 다중 정의를 제공한다면 ODR(One definition rule)을 어기게 됩니다. 따라서 동일하게 인스턴스화된 함수에 대한 처리 방법에 대해 알아보도록 하겠습니다.
탐욕스러운 인스턴스화
탐욕스러운 인스턴스화는 링커가 특정 실체가 사실 다양한 오브젝트 파일과 라이브러리 상에 중첩해서 나타날 수 있다는 것을 알고 있다고 가정합니다. 컴파일러는 일반적으로 이러한 실체를 특별한 방식으로 표시하고 링커가 다중의 인스턴스를 발견하면 하나만 남기고 나머지는 모두 버립니다. 이러한 방법에는 다음과 같은 단점이 존재합니다.
- 컴파일러는 하나만을 유지하는 데도 여러개를 인스턴스화하고 최적화해야 하기에 시간을 낭비합니다.
- 한 템플릿의 여러 인스턴스 사이에도 사소한 차이가 존재할 수 있는데 링커에서는 완전히 같은 지에 대한 검사는 수행하지 않고 처리를 하게 되는데, 만약 에러가 발생한다면 링커는 이 두개의 경우에 대한 차이를 인지하지 못합니다.
- 같은 코드가 많이 중복될 수 있기에 다른 방식에 비해 모든 오브젝트 파일 크기의 합이 커질 수 있습니다.
하지만 단점보다는 전통적인 소스-오브젝트 종속 관계가 유지되기 때문에 다른 방식에 비해 훨씬 선호됩니다. 각 오브젝트 파일은 해당 소스 파일에서 생성된 모든 링크 가능한 정의에 대한 컴파일된 코드를 가집니다.
또 다른 중요한 장점으로 모든 함수 템플릿 인스턴스화는 비싼 링크 시간 최적화 메커니즘에 의지하지 않고도 인라인의 후보가 될 수 있다는 점입니다.
일반적으로 중첩되고 유출된 인라인 함수(spilled inline function)과 가상 함수 테이블을 처리하기 위한 링크 가능한 실체의 정의를 중첩할 수 있게 하는 링크 매커니즘이 사용됩니다.
질의 인스턴스화
질의 인스턴스화는 한 프로그램에 속하는 모든 번역 단위를 컴파일할 때에 공유하는 데이터베이스가 존재하며, 이를 기반으로 어떤 특수화가 인스턴스화됐고 어떤 소스코드에 종속돼 있는지를 추적합니다. 앞서 언급한 정보와 생성된 특수화 실체 모두를 데이터베이스에 저장하여, 링크 가능한 실체에 대한 인스턴스화 지점이 나타날 때마다 다음과 같은 세 가지 중 한 가지 일이 벌어질 수 있습니다.
- 어떠한 특수화도 사용할 수 없습니다. 이 경우 인스턴스화가 일어나고 그 결과로 얻은 특수화를 데이터베이스에 저장합니다.
- 저장된 특수화를 사용할 수 있긴 하지만 이 특수화가 저장된 후 소스코드가 바뀌었으므로 새로 생성해야 합니다. 따라서 새로 인스턴스화한 후 데이터 베이스에 저장된 것 대신에 새로 저장합니다.
- 데이터 베이스에 최신의 특수화가 저장돼 있습니다. 아무것도 할 필요가 없습니다.
개념 상으로는 간단하지만 구현 상으로는 어려운 부분들이 존재합니다
- 데이터 베이스에서 소스 코드의 상태에 따라 올바른 종속 관계를 유지하는 것은 쉬운 일이 아닙니다.
- 동시에 많은 소스 파일을 컴파일하는 것은 매우 흔하기에 고 성능 컴파일러에서는 데이터 베이스에서 적절한 정도의 동시 접속 제어를 제공해야 합니다.
이 해결책의 경우 효율적으로 구현될 수 있기에 탐욕스러운 인스턴스화에 비해 시간을 덜 잡아먹습니다. 하지만 데이터 베이스를 사용하게 된다면 대부분의 C 컴파일러에서 상속된 전통적인 컴파일 모델이 적용되지 않고, 라이브러리를 사용할 때 중첩된 인스턴스화를 해결하기 위한 처리로 인한 링크 에러가 발생할 수 있습니다.
결국은 이도 폐기되었습니다.
14.5 명시적 인스턴스화
템플릿 특수화를 위한 인스턴스화 지점을 명시적으로 생성할 수도 있습니다. 이를 명시적 인스턴스화 지시자라고 합니다. 이는 키워드 template 뒤에 인스턴스화돼야 할 특수화의 선언을 넣는 방식입니다.
template<typename T>
void f(T) {};
template void f<int>(int);
수동 인스턴스화
빌드 시간을 개선하기 위해 프로그램에 필요한 템플릿 특수화를 한곳에 모아 수동으로 인스턴스화시키고, 다른 번역 단위에서는 관련 인스턴스화를 억제하는 기법도 존재합니다. 이러한 인스턴스화 억제가 이식 가능하려면 템플릿 정의를 명시적으로 인스턴스화된 번역 단위를 제외한 나머지 번역 단위에서는 아예 템플릿 정의를 제공하지 않아야 합니다.
// 번역 단위 1
template<typename T> void f(); // 정의 없음: 이 번역 단위에서 인스턴스화 방지
void g() {
f<int>();
}
// 번역 단위 2
template<typename T> void f() { };
template void f<int>(); // 수동 인스턴스화
void g();
int main() {
g();
}
첫 번쨰 번역 단위에서는 f의 정의를 보지 못하기에 f<int>에 대한 인스턴스화를 생성 못합니다. 두 번째 번역 단위에서 존재하기에 링크에서 이어지게 됩니다. 수동 인스턴스화는 어떤 실체가 인스턴스화될지 주의 깊게 추적해야 하기에 추천되지 않습니다. 하지만 몇가지 장점이 존재하는데 프로그램의 요구에 맞춰 인스턴스화 가능하며, 헤더의 크기가 줄어듭니다.
명시적 인스턴스화 선언
중복된 자동 인스턴스화를 제거하는 좀 더 맞춤형 방식으로는 명시적 인스턴스화 선언이 존재합니다. extern 키워드가 앞에 붙은 명시적 인스턴스화를 지시자를 가리키게 하는 것입니다. 일반적으로는 자동 인스턴스화가 억제되지만 예외도 많이 존재합니다.
- 인라인 함수는 인라인으로 확장시키기 위해 여전히 인스턴스화 될 수 있습니다.
- 연역된 auto나 decltype(auto) 형을 갖는 변수나 연역된 반환형을 갖는 함수는 자신의 형식을 결정하기 위해 인스턴스화 됩니다.
- 그 값이 상수 표현식에 쓰일 수 있는 변수는 그 값을 계산하기 위해 인스턴스화 될 수 있습니다.
- 참조자형의 변수는 참조하는 실체를 해석할 수 있게 인스턴스화될 수 있습니다.
- 클래스 템플릿과 별칭 템플릿은 결과 형식을 검사하기 위해 인스턴스화 될 수 있습니다.
각 명시적 인스턴스화 선언은 명시적 인스턴스화 선언과 쌍을 이뤄야만 합니다. 정의를 생략하면 링커 오류가 발생합니다.
14.6 컴파일 과정 if문
컴파일 과정 If는 if문으로, if 키워드 바로 다음에 constexpr 키워드가 뒤따라와 괄호로 싸인 조건은 상수 boolean 값을 가져야만 합니다. 그러면 컴파일러는 어떤 가지를 선택해야 하는지 알 수 있습니다. 선택되지 않은 가지는 버린 가지라고 부릅니다. 인스턴스화되는 동안 선택된 가지만이 인스턴스화되며, 버린 가지 쪽에 유효하지 않은 표현식이 있더라도 버려지기에 문제가 없게 됩니다(문법적 오류가 아닌 이상).
'C++공부 > C++ Templates' 카테고리의 다른 글
16. 특수화와 오버로딩 (0) | 2022.06.24 |
---|---|
15. 템플릿 인자 영역 (0) | 2022.06.22 |
13. 템플릿에서 이름 (0) | 2022.06.18 |
12. 템플릿 기초 원리 상세 학습 (0) | 2022.06.17 |
11. 일반 라이브러리 (0) | 2022.06.12 |