헤더 파일에서만 템플릿을 구현할 수있는 이유는 무엇입니까? 인라인 함수를 사용하여 헤더 파일로 템플릿을

C ++ 표준 라이브러리 에서 인용 : 튜토리얼 및 핸드북 :

현재 템플릿을 사용하는 유일한 이식 방법은 인라인 함수를 사용하여 헤더 파일로 템플릿을 구현하는 것입니다.

왜 이런거야?

(설명 : 헤더 파일이 유일한 휴대용 솔루션 은 아니지만 가장 편리한 휴대용 솔루션입니다.)



답변

주의 사항 : 구현을 헤더 파일에 넣을 필요 는 없습니다 .이 답변 끝에있는 대체 솔루션을 참조하십시오.

어쨌든 코드가 실패하는 이유는 템플릿을 인스턴스화 할 때 컴파일러가 주어진 템플릿 인수로 새 클래스를 생성하기 때문입니다. 예를 들면 다음과 같습니다.

template<typename T>
struct Foo
{
    T bar;
    void doSomething(T param) {/* do stuff using T */}
};

// somewhere in a .cpp
Foo<int> f; 

이 행을 읽을 때 컴파일러는 FooInt다음과 같은 새 클래스를 작성합니다 (호출하자 ).

struct FooInt
{
    int bar;
    void doSomething(int param) {/* do stuff using int */}
}

따라서 컴파일러는 메소드의 구현에 액세스하여 템플리트 인수 (이 경우 int) 를 사용하여 메소드를 인스턴스화해야합니다 . 이러한 구현이 헤더에 없으면 액세스 할 수 없으므로 컴파일러에서 템플릿을 인스턴스화 할 수 없습니다.

이에 대한 일반적인 해결책은 헤더 파일에 템플릿 선언을 작성한 다음 구현 파일 (예 : .tpp)에서 클래스를 구현하고이 구현 파일을 헤더 끝에 포함시키는 것입니다.

Foo.h

template <typename T>
struct Foo
{
    void doSomething(T param);
};

#include "Foo.tpp"

Foo.tpp

template <typename T>
void Foo<T>::doSomething(T param)
{
    //implementation
}

이런 식으로 구현은 여전히 ​​선언과 분리되어 있지만 컴파일러가 액세스 할 수 있습니다.

대체 솔루션

또 다른 해결책은 구현을 분리하여 유지하고 필요한 모든 템플릿 인스턴스를 명시 적으로 인스턴스화하는 것입니다.

Foo.h

// no implementation
template <typename T> struct Foo { ... };

Foo.cpp

// implementation of Foo's methods

// explicit instantiations
template class Foo<int>;
template class Foo<float>;
// You will only be able to use Foo with int or float

내 설명이 충분히 명확하지 않으면 이 주제에 대한 C ++ Super-FAQ을 살펴볼 수 있습니다 .


답변

여기에 많은 정답이 있지만 (완전성을 위해) 이것을 추가하고 싶었습니다.

구현 cpp 파일의 맨 아래에서 템플릿에 사용될 모든 유형을 명시 적으로 인스턴스화하면 링커에서 평소처럼 찾을 수 있습니다.

편집 : 명시 적 템플릿 인스턴스화 예제 추가. 템플릿이 정의되고 모든 멤버 함수가 정의 된 후에 사용됩니다.

template class vector<int>;

이렇게하면 클래스와 모든 멤버 함수가 인스턴스화되어 (링커가 사용할 수있게됩니다) 전용입니다. 템플릿 함수에도 비슷한 구문이 적용되므로 비 멤버 연산자 오버로드가있는 경우 이와 동일한 구문을 수행해야합니다.

일반적인 include 파일 (사전 컴파일 된 헤더?)이 벡터를 사용 extern template class vector<int>하는 다른 모든 파일 (1000?) 에서 인스턴스화되지 않도록하기 위해 벡터가 헤더에 완전히 정의되어 있기 때문에 위 예제는 상당히 쓸모가 없습니다 .


답변

별도의 컴파일이 필요하고 템플릿이 인스턴스화 스타일의 다형성이기 때문입니다.

설명을 위해 콘크리트에 조금 더 가까워 봅시다. 다음 파일이 있다고 가정 해보십시오.

  • foo.h
    • 의 인터페이스를 선언 class MyClass<T>
  • foo.cpp
    • 구현을 정의 class MyClass<T>
  • bar.cpp
    • 사용 MyClass<int>

별도의 컴파일은 bar.cpp와 독립적으로 foo.cpp 를 컴파일 할 수 있어야 함을 의미합니다 . 컴파일러는 각 컴파일 단위에서 분석, 최적화 및 코드 생성의 모든 어려운 작업을 완전히 독립적으로 수행합니다. 전체 프로그램 분석을 수행 할 필요가 없습니다. 전체 프로그램을 한 번에 처리해야하는 것은 링커 일 뿐이며 링커의 작업이 훨씬 쉽습니다.

foo.cpp를 컴파일 할 때 bar.cpp 가 존재할 필요조차 없지만, 나는 이미 foo.o를 이미 bar 와 함께 연결할 수 있어야 합니다 .o foo 를 다시 컴파일 할 필요없이 방금 생성했습니다. .cpp . foo.cpp 는 동적 라이브러리로 컴파일하고 foo.cpp 없이 다른 곳에 배포 하고 foo.cpp를 쓴 후 몇 년 동안 작성한 코드와 연결할 수 있습니다.

“Instantiation-style polymorphism”은 템플릿 MyClass<T>이 실제로 모든 값을 처리 할 수있는 코드로 컴파일 될 수있는 일반 클래스가 아님을 의미합니다 T. 즉, 등 할당 자 및 생성자, C ++ 템플릿 의도 쓰는 것을 피하기 위해되는 함수 포인터를 전달하기 위해 필요한, 권투로 오버 같은 추가 것이 거의 동일한 class MyClass_int, class MyClass_float등, 그러나 여전히 컴파일 된 코드로 끝날 수있을 대부분 것처럼 우리는 했다 별도로 각 버전을 썼다. 템플릿은 말 그대로 템플릿입니다. 수업 템플릿은 수업이 아니며 , T우리가 만날 때 마다 새로운 수업을 만들기위한 레시피입니다 . 템플릿은 코드로 컴파일 할 수 없으며 템플릿 인스턴스화 결과 만 컴파일 할 수 있습니다.

따라서 foo.cpp 가 컴파일되면 컴파일러는 bar.cpp가이 를 알아야한다는 것을 알 수 없습니다 MyClass<int>. 템플릿을 볼 수는 MyClass<T>있지만 코드를 생성 할 수는 없습니다 (클래스가 아니라 템플릿입니다). 그리고 bar.cpp 가 컴파일되면 컴파일러는을 만들어야한다는 것을 MyClass<int>알 수 있지만 템플릿 MyClass<T>( foo.h 의 인터페이스 만 )을 볼 수 없으므로 만들 수 없습니다.

경우 foo.cpp에 자신이 사용하는 MyClass<int>컴파일하는 동안, 그것을 위해 다음 코드가 생성됩니다 foo.cpp의를 그렇게 할 때, bar.o가 연결되어 foo.o 가 매여 할 수 있으며 작동합니다. 이 사실을 사용하여 단일 템플릿을 작성하여 유한 템플릿 인스턴스화를 .cpp 파일로 구현할 수 있습니다. 그러나 bar.cpp 가 템플릿을 템플릿 으로 사용하여 원하는 유형으로 인스턴스화 할 수있는 방법이 없습니다 . foo.cpp 작성자 가 제공 한 템플릿 클래스의 기존 버전 만 사용할 수 있습니다 .

템플릿을 컴파일 할 때 컴파일러는 “모든 버전을 생성”해야하며, 링크하는 동안 사용되지 않은 버전을 필터링해야한다고 생각할 수 있습니다. 포인터 및 배열과 같은 “유형 수정 자”기능을 사용하면 내장 유형만으로도 무한한 유형의 유형을 생성 할 수 있기 때문에 엄청난 오버 헤드와 이러한 접근 방식이 직면하는 극심한 어려움 외에도 이제 프로그램을 확장하면 어떻게됩니까? 추가하여:

  • baz.cpp
    • 선언하고 구현 class BazPrivate하고 사용합니다.MyClass<BazPrivate>

우리가 아니면 이것이 작동 할 수있는 가능한 방법은 없습니다

  1. 다시 컴파일해야 foo.cpp에 우리가 변경할 때마다 프로그램에서 다른 파일을 경우, 그것은 새로운 새로운 인스턴스를 추가MyClass<T>
  2. baz.cpp 가 컴파일 MyClass<T>하는 MyClass<BazPrivate>동안 baz.cpp를 생성 할 수 있도록 baz.cpp에 전체 템플릿을 포함 해야합니다 (헤더 포함을 통해). .

전체 프로그램 분석 컴파일 시스템은 컴파일하는 데 시간이 오래 걸리고 소스 코드없이 컴파일 된 라이브러리를 배포 할 수 없기 때문에 아무도 (1)을 좋아하지 않습니다. 그래서 우리는 (2)를 가지고 있습니다.


답변

템플릿은 실제로 객체 코드로 컴파일하기 전에 컴파일러에서 인스턴스화 해야합니다 . 이 인스턴스화는 템플릿 인수가 알려진 경우에만 달성 할 수 있습니다. 이제 템플릿 함수가로 선언되고 a.h정의 a.cpp되고 사용되는 시나리오를 상상해보십시오 b.cpp. a.cpp컴파일 될 때 예정된 b.cpp특정 인스턴스는 물론 다가오는 컴파일 에 템플릿 인스턴스가 필요 하다는 것을 반드시 알 필요는 없습니다 . 더 많은 헤더 및 소스 파일의 경우 상황이 더 복잡해질 수 있습니다.

템플릿의 모든 용도에 대해 컴파일러가 더 똑똑해 보일 수 있다고 주장 할 수 있지만 재귀 적이거나 복잡한 시나리오를 만드는 것은 어렵지 않을 것이라고 확신합니다. AFAIK, 컴파일러는 그러한 전망을하지 않습니다. Anton이 지적한 것처럼 일부 컴파일러는 템플릿 인스턴스화의 명시 적 내보내기 선언을 지원하지만 모든 컴파일러가이를 지원하지는 않습니다 (아직?).


답변

사실, C ++ (11) 이전에 표준이 정의 된 export키워드 것이 가능 헤더 파일에 템플릿을 선언하고 다른 곳을 구현합니다.

인기있는 컴파일러는이 키워드를 구현하지 않았습니다. 내가 아는 유일한 방법은 Comeison C ++ 컴파일러가 사용하는 Edison Design Group이 작성한 프론트 엔드입니다. 컴파일러는 적절한 인스턴스화를 위해 템플릿 정의가 필요하기 때문에 헤더 파일에 템플릿을 작성해야했습니다.

결과적으로 ISO C ++ 표준위원회는 exportC ++ 11에서 템플릿 의 기능 을 제거하기로 결정했습니다 .


답변

표준 C ++에는 이러한 요구 사항이 없지만 일부 컴파일러에서는 사용되는 모든 변환 단위에서 모든 함수 및 클래스 템플릿을 사용할 수 있어야합니다. 실제로 이러한 컴파일러의 경우 템플릿 함수 본문을 헤더 파일에서 사용할 수 있어야합니다. 반복 : 즉, 해당 컴파일러는 .cpp 파일과 같은 헤더가 아닌 파일에서 정의 할 수 없습니다.

이 문제를 완화시키기 위한 내보내기 키워드가 있지만 이식성이 거의 없습니다.


답변

컴파일러는 템플릿 매개 변수에 대해 주어진 / 제거 된 매개 변수에 따라 다른 버전의 코드를 인스턴스화해야하므로 템플릿을 헤더에 사용해야합니다. 템플릿은 코드를 직접 나타내는 것이 아니라 해당 코드의 여러 버전에 대한 템플릿입니다. .cpp파일 에서 템플릿이 아닌 함수를 컴파일하면 구체적인 함수 / 클래스가 컴파일됩니다. 템플릿의 경우에는 다른 유형으로 인스턴스화 할 수 있습니다. 즉, 템플릿 매개 변수를 콘크리트 유형으로 바꿀 때 콘크리트 코드가 생성되어야합니다.

export별도의 컴파일에 사용되는 키워드 기능이있었습니다 . 이 export기능은 더 이상 사용되지 않으며 C++11AFAIK에서는 하나의 컴파일러 만 구현했습니다. 를 사용해서는 안됩니다 export. 별도의 편집은 가능하지 않다 C++거나 C++11하지만 어쩌면의 C++17개념에서 그것을 만들 경우, 우리는 별도의 컴파일의 몇 가지 방법이 있었다.

별도의 컴파일을 수행하려면 별도의 템플릿 본문 검사가 가능해야합니다. 개념으로 해결책이 가능한 것 같습니다. 최근 표준위원회 회의에서 발표 된 이 문서를 살펴보십시오 . 사용자 코드에서 템플릿 코드의 코드를 인스턴스화해야하기 때문에 이것이 유일한 요구 사항은 아니라고 생각합니다.

템플릿에 대한 별도의 컴파일 문제 또한 현재 작동중인 모듈로의 마이그레이션으로 인해 발생하는 문제라고 생각합니다.