템플릿 기반 C ++ 클래스를 .hpp / .cpp 파일로 분할-가능합니까?


97

.hpp.cpp파일 사이에 분할 된 C ++ 템플릿 클래스를 컴파일하려고하면 오류가 발생 합니다.

$ g++ -c -o main.o main.cpp  
$ g++ -c -o stack.o stack.cpp   
$ g++ -o main main.o stack.o  
main.o: In function `main':  
main.cpp:(.text+0xe): undefined reference to 'stack<int>::stack()'  
main.cpp:(.text+0x1c): undefined reference to 'stack<int>::~stack()'  
collect2: ld returned 1 exit status  
make: *** [program] Error 1  

내 코드는 다음과 같습니다.

stack.hpp :

#ifndef _STACK_HPP
#define _STACK_HPP

template <typename Type>
class stack {
    public:
            stack();
            ~stack();
};
#endif

stack.cpp :

#include <iostream>
#include "stack.hpp"

template <typename Type> stack<Type>::stack() {
        std::cerr << "Hello, stack " << this << "!" << std::endl;
}

template <typename Type> stack<Type>::~stack() {
        std::cerr << "Goodbye, stack " << this << "." << std::endl;
}

main.cpp :

#include "stack.hpp"

int main() {
    stack<int> s;

    return 0;
}

ld물론 정확합니다. 기호가 stack.o.

에 대한 대답 이 질문은 이 말한대로 내가 이미하고 있어요로서, 도움이되지 않습니다.
이 방법 이 도움 될 수 있지만 모든 단일 방법을 .hpp파일 로 옮기고 싶지는 않습니다. 그럴 필요 는 없습니까?

.cpp파일의 모든 항목을 파일 로 이동 .hpp하고 독립 실행 형 개체 파일로 링크하지 않고 모든 항목을 포함 하는 유일한 솔루션 입니까? 즉 보인다 지독하게 못생긴! 이 경우 이전 상태로 되돌리고 이름 stack.cpp을 바꾸고 stack.hpp끝낼 수 있습니다.


코드를 실제로 숨기거나 (이진 파일에서) 깨끗하게 유지하려는 경우 두 가지 훌륭한 해결 방법이 있습니다. 첫 번째 상황이지만 일반성을 줄이는 것이 필요합니다. 여기에 설명되어 있습니다. stackoverflow.com/questions/495021/…
Sheric

명시 적 템플릿 인스턴스화는 템플릿의 컴파일 시간을 줄이는 방법입니다. stackoverflow.com/questions/2351148/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

답변:


151

별도의 cpp 파일에 템플릿 클래스 구현을 작성하고 컴파일하는 것은 불가능합니다. 그렇게하는 모든 방법은 누군가가 주장하는 경우 별도의 cpp 파일 사용을 모방하는 해결 방법이지만 실제로는 템플릿 클래스 라이브러리를 작성하고 구현을 숨기기 위해 헤더 및 lib 파일과 함께 배포하려는 경우에는 불가능합니다. .

이유를 알기 위해 컴파일 과정을 살펴 보겠습니다. 헤더 파일은 컴파일되지 않습니다. 전처리 된 것입니다. 그런 다음 전처리 된 코드는 실제로 컴파일 된 cpp 파일과 결합됩니다. 이제 컴파일러가 객체에 대한 적절한 메모리 레이아웃을 생성해야하는 경우 템플릿 클래스의 데이터 유형을 알아야합니다.

사실 템플릿 클래스는 클래스가 아니라 인자로부터 데이터 타입의 정보를 얻은 후 컴파일 타임에 컴파일러에 의해 선언과 정의가 생성되는 클래스의 템플릿이라는 것을 이해해야합니다. 메모리 레이아웃을 만들 수없는 한 메서드 정의에 대한 지침을 생성 할 수 없습니다. 클래스 메서드의 첫 번째 인수는 'this'연산자입니다. 모든 클래스 메서드는 이름이 맹 글링되고 첫 번째 매개 변수가 작동하는 개체로 개별 메서드로 변환됩니다. 'this'인수는 사용자가 유효한 형식 인수로 개체를 인스턴스화하지 않는 한 컴파일러에서 사용할 수없는 템플릿 클래스의 경우 개체의 크기를 실제로 알려줍니다. 이 경우 메서드 정의를 별도의 cpp 파일에 넣고 컴파일을 시도하면 개체 파일 자체가 클래스 정보로 생성되지 않습니다. 컴파일은 실패하지 않고 개체 파일을 생성하지만 개체 파일의 템플릿 클래스에 대한 코드는 생성하지 않습니다. 이것이 링커가 개체 파일에서 기호를 찾을 수없고 빌드가 실패하는 이유입니다.

이제 중요한 구현 세부 정보를 숨기는 대안은 무엇입니까? 우리 모두가 알고 있듯이 인터페이스를 구현에서 분리하는 주요 목적은 구현 세부 사항을 바이너리 형식으로 숨기는 것입니다. 여기에서 데이터 구조와 알고리즘을 분리해야합니다. 템플릿 클래스는 알고리즘이 아닌 데이터 구조 만 나타내야합니다. 이를 통해 템플릿 화되지 않은 별도의 클래스 라이브러리 (템플릿 클래스에서 작동하는 클래스)에서 더 중요한 구현 세부 정보를 숨기거나 데이터를 보관하는 데 사용할 수 있습니다. 템플릿 클래스에는 실제로 데이터를 할당, 가져 오기 및 설정하는 데 필요한 코드가 더 적습니다. 나머지 작업은 알고리즘 클래스에 의해 수행됩니다.

이 토론이 도움이되기를 바랍니다.


2
"템플릿 클래스는 전혀 클래스가 아님을 이해해야합니다."-그 반대가 아니 었나요? 클래스 템플릿은 템플릿입니다. "템플릿 클래스"는 때때로 "템플릿 인스턴스화"대신 사용되며 실제 클래스가됩니다.
Xupicor 2016

참고로 해결 방법이 없다고 말하는 것은 올바르지 않습니다! 데이터 구조를 메서드에서 분리하는 것도 캡슐화에 반대되기 때문에 나쁜 생각입니다. 다음과 같은 상황에서 사용할 수있는 훌륭한 해결 방법이 있습니다. stackoverflow.com/questions/495021/…
Sheric

@Xupicor, 당신이 옳습니다. 기술적으로 "클래스 템플릿"은 "템플릿 클래스"및 해당 개체를 인스턴스화 할 수 있도록 작성하는 것입니다. 그러나 일반적인 용어에서 두 용어를 서로 바꿔서 사용하는 것이 그다지 잘못된 것은 아니라고 생각합니다. "클래스 템플릿"자체를 정의하는 구문은 "클래스"가 아닌 "템플릿"이라는 단어로 시작됩니다.
Sharjith N.

@Sheric, 해결 방법이 없다고 말하지 않았습니다. 실제로 사용할 수있는 것은 템플릿 클래스의 경우 인터페이스와 구현의 분리를 모방하는 해결 방법 일뿐입니다. 특정 유형의 템플릿 클래스를 인스턴스화하지 않고는 이러한 해결 방법이 작동하지 않습니다. 어쨌든 클래스 템플릿 사용의 전체적인 요점을 해결합니다. 알고리즘에서 데이터 구조를 분리하는 것은 메소드에서 데이터 구조를 분리하는 것과 다릅니다. 데이터 구조 클래스는 생성자, 게터 및 세터와 같은 메서드를 매우 잘 가질 수 있습니다.
Sharjith N.

이 작업을 위해 방금 찾은 가장 가까운 것은 한 쌍의 .h / .hpp 파일을 사용하고 템플릿 클래스를 정의하는 .h 파일 끝에 #include "filename.hpp"를 사용하는 것입니다. (세미콜론이있는 클래스 정의의 닫는 중괄호 아래). 이것은 적어도 구조적으로 파일 단위로 분리되며, 결국 컴파일러가 .hpp 코드를 #include "filename.hpp"위에 복사 / 붙여 넣기 때문에 허용됩니다.
Artorias2718

90

그것은 이다 당신이 필요로 가고있는 것을 인스턴스화 알고있는만큼, 수.

stack.cpp 끝에 다음 코드를 추가하면 작동합니다.

template class stack<int>;

템플릿이 아닌 모든 스택 메서드가 인스턴스화되고 연결 단계가 제대로 작동합니다.


7
실제로 대부분의 사람들은이를 위해 stackinstantiations.cpp와 같은 별도의 cpp 파일을 사용합니다.
Nemanja Trifunovic

@NemanjaTrifunovic stackinstantiations.cpp가 어떻게 생겼는지 예를 들어 줄 수 있습니까?
qwerty9967 2013 년

3
사실 다른 솔루션이 있습니다 codeproject.com/Articles/48575/...
sleepsort

@ Benoît 오류 오류가 발생했습니다. ';'앞에 비정규 ID가 필요합니다. 토큰 템플릿 stack <int>; 그 이유를 아십니까? 감사!
카미노

3
실제로 올바른 구문은 template class stack<int>;.
Paul Baltescu 2014-06-10

8

이런 식으로 할 수 있습니다

// xyz.h
#ifndef _XYZ_
#define _XYZ_

template <typename XYZTYPE>
class XYZ {
  //Class members declaration
};

#include "xyz.cpp"
#endif

//xyz.cpp
#ifdef _XYZ_
//Class definition goes here

#endif

이것은 Daniweb 에서 논의되었습니다 .

또한에서 자주 묻는 질문 하지만, C ++ 수출 키워드를 사용.


5
includecpp파일을 만드는 것은 일반적으로 끔찍한 생각입니다. 이에 대한 타당한 이유가 있더라도 파일 (실제로는 영광스러운 헤더 )에 무슨 일이 일어나고 있는지 명확하게하고 실제 파일을 대상으로하는 혼란을 없애기 위해 hpp다른 확장자 (예 :)를 지정해야합니다 .tppmakefile cpp
underscore_d

@underscore_d .cpp파일을 포함하는 것이 왜 끔찍한 생각 인지 설명해 주 시겠습니까?
Abbas

1
@Abbas 확장자 cpp(또는 cc, 또는 c또는 기타)는 파일이 구현의 일부이고 결과 변환 단위 (전 처리기 출력)가 별도로 컴파일 가능하며 파일의 내용이 한 번만 컴파일됨을 나타 내기 때문입니다. 파일이 인터페이스의 재사용 가능한 부분임을 나타내지 않으며 임의의 위치에 포함됩니다. #include보내고 실제 cpp 파일을 신속하게 바르게 그래서 여러 정의 오류 화면을 작성하고있다. 가이 경우에, 이유 #include는, cpp확장 단지 잘못된 선택이었다.
underscore_d

@underscore_d 그래서 기본적으로 .cpp그러한 용도로 확장 을 사용하는 것은 잘못된 것입니다. 그러나 다른 말을 사용하는 .tpp것은 완전히 괜찮습니다. 동일한 목적을 수행하지만 더 쉽고 빠른 이해를 위해 다른 확장을 사용합니까?
Abbas

1
@Abbas 예, cpp/ cc/ 등은 피해야하지만, 이외의 사용 뭔가 좋은 아이디어이다 hpp- 예를 들어 tpp, tcc등 - 당신은 파일 이름의 나머지 부분을 재사용하고 표시 할 수 있도록 그 tpp파일은 헤더와 같은 역할을하지만, 해당에서 템플릿 선언의 아웃 오브 라인 구현을 보유합니다 hpp. 따라서이 게시물은 선언과 정의를 두 개의 서로 다른 파일로 분리하는 좋은 전제에서 시작합니다. 이는 grok / grep이 더 쉬울 수 있고 때로는 순환 종속성 IME로 인해 필요할 수 있습니다.하지만 두 번째 파일의 확장자가 잘못되었다는 제안으로 잘못 끝납니다.
underscore_d

6

아니요, 불가능합니다. export모든 의도와 목적을 위해 실제로 존재하지 않는 키워드 없이는 아닙니다 .

최선의 방법은 ".tcc"또는 ".tpp"파일에 함수 구현을 넣고 .hpp 파일 끝에 .tcc 파일을 #include하는 것입니다. 그러나 이것은 단지 화장품 일뿐입니다. 헤더 파일에서 모든 것을 구현하는 것과 동일합니다. 이것은 단순히 템플릿 사용에 대해 지불하는 가격입니다.


3
귀하의 답변이 정확하지 않습니다. 사용할 템플릿 인수를 알고 있으면 cpp 파일의 템플릿 클래스에서 코드를 생성 할 수 있습니다. 자세한 내용은 내 대답을 참조하십시오.
Benoît

2
사실이지만 이것은 .cpp 파일을 업데이트하고 템플릿을 사용하는 새로운 유형이 도입 될 때마다 다시 컴파일해야한다는 심각한 제한을 동반합니다. 이는 아마도 OP가 염두에 두었던 것이 아닐 것입니다.
Charles Salvia

3

템플릿 코드를 헤더와 cpp로 분리하려는 두 가지 주요 이유가 있다고 생각합니다.

하나는 단순한 우아함을위한 것입니다. 우리 모두는 읽고, 관리하고 나중에 재사용 할 수있는 코드를 작성하는 것을 좋아합니다.

기타는 컴파일 시간 단축입니다.

저는 현재 (항상 그렇듯이) OpenCL과 함께 시뮬레이션 소프트웨어를 코딩하고 있으며, HW 기능에 따라 필요에 따라 float (cl_float) 또는 double (cl_double) 유형을 사용하여 실행할 수 있도록 코드를 유지하고 싶습니다. 지금은 코드 시작 부분에 #define REAL을 사용하여 수행되지만 매우 우아하지는 않습니다. 원하는 정밀도를 변경하려면 응용 프로그램을 다시 컴파일해야합니다. 실제 런타임 유형이 없기 때문에 당분간 이것으로 살아야합니다. 운 좋게도 OpenCL 커널은 컴파일 된 런타임이며, 간단한 sizeof (REAL)를 사용하면 그에 따라 커널 코드 런타임을 변경할 수 있습니다.

훨씬 더 큰 문제는 응용 프로그램이 모듈 식이지만 보조 클래스 (예 : 시뮬레이션 상수를 미리 계산하는 클래스)를 개발할 때 템플릿 화해야한다는 것입니다. 이러한 클래스는 모두 클래스 종속성 트리의 맨 위에 한 번 이상 나타납니다. 최종 템플릿 클래스 Simulation은 이러한 팩토리 클래스 중 하나의 인스턴스를 가지므로 사실상 팩토리 클래스를 약간 변경할 때마다 전체 소프트웨어를 재 구축해야합니다. 이것은 매우 성가신 일이지만 더 나은 해결책을 찾을 수없는 것 같습니다.


2

#include "stack.cpp끝에있는 경우에만 stack.hpp. 구현이 비교적 크고 .cpp 파일의 이름을 다른 확장명으로 바꾸는 경우에만이 방법을 권장합니다.


4
이 경우 stack.cpp 파일에 #ifndef STACK_CPP (및 친구)를 추가 할 수 있습니다.
Stephen Newell

이 제안에 나를 이길. 나도 스타일상의 이유로이 접근 방식을 선호하지 않습니다.
루크

2
예, 그런 경우 두 번째 파일에 확장자 cpp( cc또는 기타)를 지정해서는 안됩니다. 이는 실제 역할과 완전히 대조되기 때문입니다. 대신 (A) 헤더와 (B) 다른 헤더 의 맨 아래 에 포함될 헤더를 나타내는 다른 확장자를 지정해야합니다 . 내가 사용하는 tpp솜씨도에 대한 설 수있는이를 위해 t그들을 p늦게 메신저 plementation (아웃 오브 라인 정의). 나는 이것에 대해 더 많은 것을 여기에서 읊었다 : stackoverflow.com/questions/1724036/…
underscore_d

2

모든 템플릿 매개 변수를 비 템플릿 클래스로 추출 할 수있는 경우 (유형이 안전하지 않을 수 있음) 공통 기능 foo를 추출 할 수 있다면 대부분의 구현을 cpp 파일에 숨길 수 있습니다. 그런 다음 헤더에는 해당 클래스에 대한 리디렉션 호출이 포함됩니다. 유사한 접근 방식이 "템플릿 팽창"문제와 싸울 때 사용됩니다.


+1-대부분의 경우 잘 작동하지 않더라도 (적어도 내가 원하는만큼 자주는 아니지만)
peterchen

2

스택이 어떤 유형과 함께 사용되는지 알고 있다면 cpp 파일에서 신속하게 인스턴스화하고 모든 관련 코드를 거기에 보관할 수 있습니다.

DLL (!)을 통해 이러한 파일을 내보낼 수도 있지만 구문을 올바르게 가져 오는 것은 매우 까다 롭습니다 (MS 별 __declspec (dllexport) 및 내보내기 키워드 조합).

우리는 double / float를 템플릿 화 한 math / geom lib에서 사용했지만 꽤 많은 코드를 가지고 있습니다. (당시 검색을했지만 오늘은 그 코드가 없습니다.)


2

문제는 템플릿이 실제 클래스를 생성하지 않고 컴파일러에게 클래스 생성 방법을 알려주는 템플릿 일 뿐이라는 것 입니다. 구체적인 클래스를 생성해야합니다.

쉽고 자연스러운 방법은 헤더 파일에 메소드를 넣는 것입니다. 그러나 다른 방법이 있습니다.

.cpp 파일에서 필요한 모든 템플릿 인스턴스화 및 메서드에 대한 참조가있는 경우 컴파일러는 프로젝트 전체에서 사용할 수 있도록 해당 템플릿을 생성합니다.

새로운 stack.cpp :

#include <iostream>
#include "stack.hpp"
template <typename Type> stack<Type>::stack() {
        std::cerr << "Hello, stack " << this << "!" << std::endl;
}
template <typename Type> stack<Type>::~stack() {
        std::cerr << "Goodbye, stack " << this << "." << std::endl;
}
static void DummyFunc() {
    static stack<int> stack_int;  // generates the constructor and destructor code
    // ... any other method invocations need to go here to produce the method code
}

8
dummey 함수가 필요하지 않습니다. Use 'template stack <int>;' 이렇게하면 템플릿을 현재 컴파일 단위로 강제 실행합니다. 템플릿을 정의하지만 공유 라이브러리에서 몇 가지 특정 구현 만 원하는 경우 매우 유용합니다.
Martin York

@Martin : 모든 멤버 함수 포함? 환상적입니다. 이 제안을 "숨겨진 C ++ 기능"스레드에 추가해야합니다.
Mark Ransom

: @LokiAstari 나는 사람이 더 배우고 싶은 경우에이에 대한 기사를 찾을 cplusplus.com/forum/articles/14272
앤드류 라르손

1

hpp 파일에 모든 것이 있어야합니다. 문제는 컴파일러가 다른 cpp 파일에 필요하다는 것을 컴파일러가 알 때까지 클래스가 실제로 생성되지 않는다는 것입니다. 따라서 해당 시점에 템플릿 클래스를 컴파일하기 위해 모든 코드를 사용할 수 있어야합니다.

내가하는 경향이있는 한 가지는 템플릿을 일반적인 비 템플릿 파트 (cpp / hpp로 나눌 수 있음)와 비 템플릿 클래스를 상속하는 유형별 템플릿 파트로 분할하는 것입니다.


1

이를 수행 할 수있는 위치는 라이브러리 및 헤더 조합을 만들고 사용자에게 구현을 숨길 때입니다. 따라서 제안 된 접근 방식은 명시 적 인스턴스화를 사용하는 것입니다. 소프트웨어가 제공 할 것으로 예상되는 것을 알고 있고 구현을 숨길 수 있기 때문입니다.

몇 가지 유용한 정보는 다음과 같습니다. https://docs.microsoft.com/en-us/cpp/cpp/explicit-instantiation?view=vs-2019

동일한 예 : Stack.hpp

template <class T>
class Stack {

public:
    Stack();
    ~Stack();
    void Push(T val);
    T Pop();
private:
    T val;
};


template class Stack<int>;

stack.cpp

#include <iostream>
#include "Stack.hpp"
using namespace std;

template<class T>
void Stack<T>::Push(T val) {
    cout << "Pushing Value " << endl;
    this->val = val;
}

template<class T>
T Stack<T>::Pop() {
    cout << "Popping Value " << endl;
    return this->val;
}

template <class T> Stack<T>::Stack() {
    cout << "Construct Stack " << this << endl;
}

template <class T> Stack<T>::~Stack() {
    cout << "Destruct Stack " << this << endl;
}

main.cpp

#include <iostream>
using namespace std;

#include "Stack.hpp"

int main() {
    Stack<int> s;
    s.Push(10);
    cout << s.Pop() << endl;
    return 0;
}

산출:

> Construct Stack 000000AAC012F8B4
> Pushing Value
> Popping Value
> 10
> Destruct Stack 000000AAC012F8B4

그러나 저는이 접근 방식을 완전히 좋아하지 않습니다. 왜냐하면 이것은 잘못된 데이터 유형을 템플릿 클래스에 전달함으로써 응용 프로그램이 스스로를 쏠 수 있기 때문입니다. 예를 들어, main 함수에서 s.Push (1.2)와 같이 암시 적으로 int로 변환 할 수있는 다른 유형을 전달할 수 있습니다. 그리고 그것은 제 생각에 나쁜 것입니다.



0

템플릿은 필요할 때 컴파일되기 때문에 다중 파일 프로젝트에 대한 제한이 적용됩니다. 템플릿 클래스 또는 함수의 구현 (정의)은 선언과 동일한 파일에 있어야합니다. 즉, 별도의 헤더 파일에서 인터페이스를 분리 할 수 ​​없으며 템플릿을 사용하는 모든 파일에 인터페이스와 구현을 모두 포함해야합니다.


0

또 다른 가능성은 다음과 같이하는 것입니다.

#ifndef _STACK_HPP
#define _STACK_HPP

template <typename Type>
class stack {
    public:
            stack();
            ~stack();
};

#include "stack.cpp"  // Note the include.  The inclusion
                      // of stack.h in stack.cpp must be 
                      // removed to avoid a circular include.

#endif

이 제안은 스타일 문제로 싫지만 당신에게 적합 할 수 있습니다.


1
포함되는 영광스러운 두 번째 헤더는 실제 소스 파일 cpp과의 혼동을 피하기 위해 최소한 다른 확장자를 가져야 합니다. 일반적인 제안에는 및 . tpptcc
underscore_d

0

'export'키워드는 템플릿 선언에서 템플릿 구현을 분리하는 방법입니다. 이것은 기존 구현없이 C ++ 표준에 도입되었습니다. 당연히 몇 개의 컴파일러 만이 실제로 구현했습니다. Inform IT 기사에서 자세한 정보를 읽어보십시오.


1
이것은 거의 링크 전용 답변이며 해당 링크는 작동하지 않습니다.
underscore_d

0

1) .h 및 .cpp 파일을 분리하는 주된 이유는 클래스의 .h를 포함하는 사용자 코드에 링크 될 수있는 별도로 컴파일 된 Obj 코드로 클래스 구현을 숨기는 것입니다.

2) 비 템플릿 클래스는 .h 및 .cpp 파일에 구체적이고 구체적으로 정의 된 모든 변수를 가지고 있습니다. 따라서 컴파일러는 컴파일 / 번역 전에 클래스에서 사용되는 모든 데이터 유형에 대한 정보가 필요합니다 . 객체 / 머신 코드 생성 템플릿 클래스는 클래스 사용자가 필요한 데이터를 전달하는 객체를 인스턴스화하기 전에 특정 데이터 유형에 대한 정보가 없습니다. 유형:

        TClass<int> myObj;

3)이 인스턴스화 후에 만 ​​컴파일러는 전달 된 데이터 유형과 일치하는 특정 버전의 템플릿 클래스를 생성합니다.

4) 따라서 .cpp는 사용자의 특정 데이터 유형을 알지 못하면 별도로 컴파일 할 수 없습니다. 따라서 사용자가 필요한 데이터 유형을 지정할 때까지 ".h"내의 소스 코드로 유지되어야하며 특정 데이터 유형으로 생성 된 다음 컴파일 될 수 있습니다.


-3

Visual Studio 2010에서 작업 중입니다. 파일을 .h 및 .cpp로 분할하려면 .h 파일 끝에 cpp 헤더를 포함하십시오.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.