작성
·
115
1
템플릿 섹션 공부 완료 후 실습을 통한 복습을하며 개인적인 질문을 스스로 질문해보고 답해봤습니다. 클래스 템플릿 부분이 잘 와닿지 않아 고심한 부분 위주로 남겨 제가 이해한게 맞는지 검토받고싶습니다. 같은 설명과 이야기가 반복될 수 있습니다ㅜ 두서없이 요약하지못해 죄송합니다.
▶아래는 템플릿이 선언(.h)과 정의(.cpp)가 나누어졌을 때 기반입니다.
템플릿 클래스와 멤버 함수들의 선언이 명시된 .h를 통해 include한 쪽은 선언이 복사되어 포함된다.
컴파일러가 .h를 포함한 곳은 번역단위가 선언만 이루어졌으니 전처리시점, 코드 작성 시점에는 템플릿을 해당 타입의 instantiation 을 할 예정인 명령어로 번역한다. 그리고 목적파일 생성 후 링커에 instantiation이 이루어질때 .cpp파일의 구현부 번역이 일어나는데, 해당 번역단위 입장에서는 인스턴스화 타입의 구현 정보가 존재하지않아 링커에 의한 오류가 생긴다.
=> 이러한 이유로 구현부를 헤더파일에 모두 포함시키거나 어떤타입의 instantiation이 이뤄지질지 explicit instantiation 명시가 구현부(.cpp)에 필요하다.
이러한 특성때문에 만약 선언부에 정의가 함께되어있는 함수의 특수화 함수가 필요하다면 기본 템플릿 함수와 함께 정의 하되, inline 키워드를 붙여 동일한 함수임을 컴파일에게 명시해야한다
왜? 헤더파일을 컴파일이 번역할때마다 기본 템플릿 함수와 특수화된 함수를 중복된 정의 함수라 판별하기때문이다.(다른 instance 템플릿 클래스 함수와 충돌이라 생각)
그렇다면? 사용할 특정 타입만 내 마음대로 특성화 함수로 기본템플릿 함수의 정의와 함께 구현부에 구현할수있을까?
할수는 있다고 생각한다. 하지만, cpp의 explicit instantiation과 다른 타입의 인스턴스로 컴파일 번역이 될 경우 문제가 발생한다.
.h에서는 인스턴스화 시에 해당 특성화 함수 정의를 할 수 있겠지만, .cpp에 해당 타입의 explicit instantiation이 명시되어 있지 않다면 특성화 제외 구현부가 나눠진 함수들은 인스턴스가 되지 못한다.
=> 헤더파일에서 정의부가 함께있는 함수는 컴파일러가 instantiation할때 타입별 template instance가 생기지만
구현부가 cpp에 있을경우 explicit instantiation 없이는 해당 타입의 instance정보가 없어 해당 함수 사용시 오류가 발생한다.
그럼? 특수화 함수 정의는 .h에! 기본템플릿 함수는 .cpp에 정의하고 특수화 함수와 동일한 타입의 template class Test<double>과 같이 explicit instantiation 해주면 되지않을까?
안된다! 정의부(.cpp)에서 기본템플릿 함수를 특수화 한 타입과 충돌에 문제가 발생한다.
이미 .cpp파일 상단에 explicit instantiation을 선언해두었지만 아래에서 중복되는 타입의 특수화 함수정의 할시
해당 인스턴스가 중복되는 이유로 중복선언된 함수가 되어버린다. inline을 써도 소용이 없다 해당 타입의 instance가 중복된거기때문이다! .h의 특수화함수 인스턴스 vs .cpp의 기본템플릿 인스턴스가 되어버린다.
explicit instantiation가 이미 인스턴스화 되어있습니다! 라는 오류 (C2908)
결론, 그래서 타입별 instance가 한정되어있다면 구현부(.cpp)에 explicit instantiation를 하고 기본템플릿 구현부를 만들어도 된다.
만약 특수화가 필요한경우 구현부에 정의된 기본템플릿 함수를 헤더파일로 옮겨, 특수화할 함수에 inline키워드를 붙이고 클래스 외부에 특수화를 진행하면 된다.
즉 템플릿 클래스내에 특수화가 필요한 멤버함수가 있다면 헤더파일에 inline키워드와 함께 구현할것
선언부에 모든 템플릿 클래스 함수를 정의하기 어렵다면? .cpp파일에 구현부를 나누되 해당 특수화 인스턴스를 명시해두고,
다양한 대응을 해야하는 함수라면 그냥 선언부에 때려넣어야한다.
▶아래는 템플릿 클래스 내 friend키워드가 지정된 외부 함수가 템플릿 매개변수를 포함하는 인스턴스를 매개변수로 가질때 입니다.
template<typename T>를 선언부에 명시 하지 않아도 되는 이유
클래스내 멤버함수의 특징인 일반 멤버 함수들도 첫 번째 매개변수를 암시적으로 this포인터를 받는다. 멤버함수로 정의가 가능한 연사자 오버로딩 함수들은 선언부를 나눠서 같은 헤더파일에 클래스 외부에 따로 구현부를 작성해도 컴파일러에 의해 알아서 매칭이 이루어진다. 그렇다는 것은 템플릿 클래스의 경우 컴파일러에 의해 template class instantiation시 해당 연산자 오버로딩 함수는 멤버함수로써 this포인터로 인스턴스를 가리키기 때문에 선언부에 template<typename T>를 따로 명시하지않아도 된다.
=> 그러나 구현부를 헤더가아닌 .cpp로 나눈경우에는 해당 함수를 번역할 시에는 template class에 대한 정보가 없으므로 template<typename T>를 모든 멤버함수에게 명시해주어야한다! 또한, 컴파일러가 번역 시 대신 특수화 함수가 함께 선언되는경우에는 중복정의가 아닌 같은 함수임을 명시하는 inline을 명시해야만 한다.
템플릿 클래스의 외부 함수 friend 명시와 매개변수에 따른 매칭의 모호함
외부함수로 정의하여 오버로딩해야하기 때문에 해당 인스턴스 매개변수의 멤버 변수에 접근하겠다는 의미로 friend를 붙여서 오버로딩을 구현시킨다.
템플릿 클래스의경우 operaotr <<는 friend임을 선언부에 명시하게되면 해당 함수의 선언 보다는 friend 키워드를 사용하는 외부함수 명시에 불과한데(이 외부함수는 friend로써 해당 인스턴스의 멤버 변수 및 함수에 접근가능해!)
이어서 동일한 헤더파일의 클래스 외부에 인스턴스 시 template 타입과 동일하게 하기위해 temaplte<typename T>를 붙여 정의를 하게된다.
이 때, 템플릿 매개변수 T를 변수의 타입으로써 함수의 매개변수로 사용할 때는 컴파일러가 friend 외부함수 선언과 매칭 시키는것으로 번역한다. 그런데!!! 일반타입이아닌 템플릿 클래스 인스턴스를 (MyArray<T>) 매개변수로 사용하게 될때 문제가 발생한다.
템플릿 클래스의 인스턴스를 템플릿 매개변수 T=> int의 인스턴스를 만들 때 컴파일러 번역에서 friend키워드가 붙은 외부함수의 매개변수 MyArray<T>를 MyArray<int>로 일반화하여 해석한다.
때문에 정의부를 클래스 바깥 외부에 temaplate<typename T>로 정의하여 매칭을 유도시켜봤자, 컴파일의 해석에 따른 다른 인스턴스 취급을 받게되어 매칭으로 이어지지 못한다.
때문에 클래스 내에 friend로 선언된 operator << 는 선언만 있게되는 외부 함수가 되고 정작 참조할 정의부가 없어 오류가 생겼던것이다.
그래서 해당 함수를 템플릿 클래스 인스턴스 할때 함께 포함될 외부함수라는것을 컴파일러에게 알려주기 위해 template<typename T>를 따로 명시해줘야한다.
반면, 템플릿 매개변수가 일반타입일 경우에는 매칭이 된다.
답변 2
0
안녕하세요, 질문&답변 도우미 durams입니다.
instantiation 시점과 링킹에 대해 오해가 있으신 것 같습니다.
템플릿의 instantiation은 컴파일 타임의 후반부에 일어납니다. 컴파일러가 구문을 분석하다가 특정 템플릿을 사용하는 부분을 발견하면 그때 요구하는 타입으로 instantiation이 수행되죠. 그러니 번역(compilation)이 끝난 시점에는 이미 instantiation도 완료되어 있습니다.
전처리시점, 코드 작성 시점에는 템플릿을 해당 타입의 instantiation 을 할 예정인 명령어로 번역한다.
: 컴파일 시점을 코드 작성 시점으로 잘못 쓰신것이 아닌가 싶습니다. 그리고 전처리는 컴파일 이전에 #include
, #define
등과 같은 전처리 지시문을 처리하는 과정이며 번역이 일어나지 않습니다.
해당 번역단위 입장에서는 인스턴스화 타입의 구현 정보가 존재하지않아 링커에 의한 오류가 생긴다.
는 맞는 말이지만, 링커에 instantiation이 이루어질때 .cpp파일의 구현부 번역이 일어나는데
는 틀린 말입니다. 링커는 따로 번역을 진행하지 않습니다. 링커는 여러 translation unit 간의 심볼 정보를 연관짓고, object file들을 합쳐서 executable file을 만드는 역할입니다.
화살표 이하의 내용은 맞습니다. 여러 헤더 파일에서 특수화된 구현을 include
하게되면 ODR(One Definition Rule) 위반이 일어나기 때문에, 방지하기 위해서 inline
으로 명시할 수 있습니다.
여러번 읽고 제 나름대로 해석해서 예제를 구성해봤습니다. 아마 이러한 상황을 말씀하시는 것 같습니다.
// test.h
#include <iostream>
template<typename T>
class Test {
public:
T val;
void print();
};
// test.cpp
#include "test.h"
template<typename T>
inline void Test<T>::print() {
std::cout << val << '\n';
}
template<>
inline void Test<char>::print() {
std::cout << "char : " << val << '\n';
}
template<>
inline void Test<int>::print() {
std::cout << "int : " << val << '\n';
}
// main.cpp
#include <iostream>
#include "test.h"
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
Test<int> x;
x.val = 3;
x.print();
Test<char> y;
y.val = 'D';
y.print();
return 0;
}
헤더파일에서 정의부가 함께있는 함수는 컴파일러가 instantiation할때 타입별 template instance가 생기지만 구현부가 cpp에 있을경우 explicit instantiation 없이는 해당 타입의 instance정보가 없어 해당 함수 사용시 오류가 발생한다.
라는 말은 위 상황에서 링킹 에러가 발생한다는 말씀이신 것 같습니다.
조금 더 복잡한 개념인데요, 이건 사실 상황에 따라 다릅니다. 해당 타입의 instance정보가 없어 해당 함수 사용시 오류가 발생한다.
에서 instance 정보가 없다는게 링킹 시점에서 템플릿 정의를 다른 translation unit에서 알 수 없다는 뜻인것 같은데, 실제로는 그리 간단하지는 않습니다.
템플릿 특수화 시, 해당 함수는 컴파일러에게 일반 함수처럼 취급되며, 컴파일 타임에 instantiation을 하는 것이 아니라 링킹 시점에 전체 translation unit에서 정의를 찾게 됩니다. 그런데 함수를 inline
으로 명시하면 해당 함수를 사용하는 translation unit 안에서는 인라인 함수의 정의가 반드시 들어 있어야만 합니다. 그래서 test.cpp
에서 특수화 시 inline
으로 명시하였지만, 이는 main.cpp
와 다른 translation unit에서 볼 수 없게 되고 링킹 에러가 발생합니다.
The definition of an inline function or variable(since C++17) must be reachable in the translation unit where it is accessed (not necessarily before the point of access).
해결하는 방법은 2가지가 있습니다.
첫 번째는 inline
을 없애는겁니다.
#include "test.h"
template<typename T>
inline void Test<T>::print() {
std::cout << val << '\n';
}
template<>
void Test<char>::print() {
std::cout << "char : " << val << '\n';
}
template<>
void Test<int>::print() {
std::cout << "int : " << val << '\n';
}
위에서 설명드렸듯이, 템플릿 특수화를 진행한 경우 링킹 시점에 정의를 찾아가게 되므로 문제가 없습니다.
제가 특수화하지 않은 기본 print()
는 inline
을 그대로 뒀는데, 만약 main.cpp
에서 Test<double>
타입 클래스를 생성해서 print()
를 호출한다면 당연히 에러가 발생하게 됩니다. 특수화 여부와 상관없이 함수의 정의 자체를 main.cpp
의 translation unit에서 보지 못하기 때문입니다.
애초에 inline
자체가 헤더 파일을 여러 translation unit에서 포함했을 때의 ODR 위반을 방지하기 위한 것이라 .cpp
파일에서 쓰기에는 적절하지 않으며, 필요하지도 않습니다.
정리하자면 아래와 같습니다.
'템플릿 특수화는 컴파일 타임에 instantiation이 일어나지 않고 명시된 정의를 사용하므로, 일반 함수처럼 .cpp
파일에 정의를 둘 수 있다. 만약 헤더 파일에 정의를 두는 경우, ODR 위반 방지를 위해 inline
을 추가적으로 명시해야 한다.'
템플릿 특수화 정의는 기본적으로 inline
속성이 붙어있지 않기 때문에 .cpp
파일에 작성하는 것도 매끄럽다고 생각합니다. (클래스 내부의 함수가 암시적으로 inline
속성을 가진 것과는 대조적입니다)
두 번째는 explicit instantiation을 해주는겁니다.
#include "test.h"
template<typename T>
void Test<T>::print() {
std::cout << val << '\n';
}
template<>
inline void Test<char>::print() {
std::cout << "char : " << val << '\n';
}
template void Test<char>::print();
template<>
inline void Test<int>::print() {
std::cout << "int : " << val << '\n';
}
template void Test<int>::print();
이렇게 한 경우에도 실행이 됩니다. inline
으로 설정했지만, explicit instantiation으로 명시적으로 함수의 정의를 object file에 포함시켜서 링킹 시점에 main.cpp
에서 알 수 있게 하는 것으로 보입니다.
inline
으로 정의한 템플릿 특수화 이후 explicit instantiation을 했을 때 어떠한 일이 발생하는지는 저도 확실히 아는 사항이 아니었기 때문에, 깊게 생각해보았습니다.
일반적인 템플릿 함수는 해당 translation unit에서 사용하는 것이 아니라면 instantiation되지 않는다. 대신 explicit instantiation을 해주게 되면 다른 translation unit에서 링킹 시점에 정의를 찾을 수 있게 된다.
하지만 위 예시는 일반적인 경우가 아니라 템플릿 특수화에 해당한다. 특수화는 instantiation과는 달리 해당 함수의 정의 자체를 object file에 포함시키는 효과를 지닌다.
하지만 inline
특수화만 있을 때에는 오히려 링킹 에러가 발생하고, 추가로 explicit instantiation을 해주면 에러가 사라진다. 그렇다면 inline
특수화 정의는 object file에 함수의 정의를 포함시키지 않을 수도 있는 것으로 보인다.
잠정적인 결론으로, explicit instantiation을 함으로써 이전에 inline
으로 특수화가 되었다 하더라도 링킹 시 다른 translation unit에서 찾을 수 있게 되었다는 생각이 들긴 했지만, 여러 자료를 찾아봐도 관련된 사례를 찾을 수가 없어서 증명은 하지 못한 상태입니다. inline
키워드의 목적과도 아예 다른 코드이기 때문에 상당히 적절하지 않은 형태라고도 생각합니다. C++ 표준을 더 찾아보고 보충할 내용이 있다면 추가로 답변드리겠습니다.
추가로 궁금하실까 싶어 아래 형태에 대해서도 말씀드립니다.
#include "test.h"
template<typename T>
void Test<T>::print() {
std::cout << val << '\n';
}
template<>
void Test<char>::print() {
std::cout << "char : " << val << '\n';
}
template void Test<char>::print();
template<>
void Test<int>::print() {
std::cout << "int : " << val << '\n';
}
template void Test<int>::print();
이 형태도 실행은 됩니다만, 여기서는 explicit instantiation이 오히려 필요가 없습니다.
Explicit instantiation has no effect if an explicit specialization appeared before for the same set of template arguments.
죄송하지만 이 부분은 제가 글을 이해할 수가 없네요. 대신 2번 섹션과 비슷한 내용으로 생각되는데, 아마 앞에서 충분히 설명이 되었을 것이라고 생각합니다.
맞습니다.
맞습니다. friend
로 템플릿 클래스 내에 함수를 선언했더라도, 해당 함수 자체는 클래스의 멤버도 아니고 템플릿 함수도 아니죠. 대신 템플릿 클래스가 instantiation될 때 주어진 타입에 따라 생성될 뿐입니다.
그렇기 때문에 클래스 템플릿 내부에 함수의 정의를 하지 않는다면, instantiation 시 해당 템플릿 인자에 맞는 friend
함수의 정의가 생성되지 않습니다. 그 결과로 아래 코드의 경우 링킹 에러가 발생하게 됩니다.
#include <iostream>
template<typename T>
class Test {
public:
T val;
void print();
friend std::ostream& operator<<(std::ostream& out, const Test& test);
};
template<typename T>
std::ostream& operator<<(std::ostream& out, const Test<T>& test) {
out << test.val;
return out;
}
말씀하신 문제의 해결 방법으로는 첫 번째로 friend
함수의 정의까지 템플릿 클래스 내부에 작성하는 방법이 있겠습니다. 추천되는 방식이라고 생각합니다.
#include <iostream>
template<typename T>
class Test {
public:
T val;
void print();
friend std::ostream& operator<<(std::ostream& out, const Test& test) {
out << test.val;
return out;
}
};
또는 말씀하신 방법을 사용할 수 있습니다만, 사실 아래 코드에는 문제가 있습니다.
#include <iostream>
template<typename T>
class Test {
public:
T val;
void print();
template<typename T>
friend std::ostream& operator<<(std::ostream& out, const Test& test);
};
template<typename T>
std::ostream& operator<<(std::ostream& out, const Test<T>& test) {
out << test.val;
return out;
}
T
라는 template parameter를 중복으로 선언했기 때문에, 그 parameter간에 shadowing이 발생하게 됩니다.
The name of a template parameter shall not be bound to any following declaration whose locus is contained by the scope to which the template parameter belongs.
위 코드는 어째선지 MSVC에서는 에러가 발생하지 않고 그대로 컴파일이 가능하지만, 동일한 코드를 g++ 로 컴파일해보면 아래와 같이 에러가 발생합니다.
그래서 권장하지 않습니다. 예제와 같은 간단한 프로그램의 경우에는 눈에 보이는 문제가 발생하지 않을수도 있지만, 프로그램 규모가 커지게 되면 lookup 과정에서 예상치 못한 에러가 발생할겁니다.
대신 아래와 같이 함수/연산자 자체를 템플릿화해도 해결이 가능합니다.
#include <iostream>
template<typename T>
class Test;
template<typename T>
std::ostream& operator<<(std::ostream& out, const Test<T>& test);
template<typename T>
class Test {
public:
T val;
void print();
friend std::ostream& operator<< <>(std::ostream& out, const Test& test);
};
template<typename T>
std::ostream& operator<<(std::ostream& out, const Test<T>& test) {
out << test.val;
return out;
}
위 코드를 설명하면 아래와 같습니다.
함수를 템플릿화하기 위해 함수의 name 뒤에 <>
를 추가.
대신 함수 선언은 클래스 템플릿 정의 이전에 있어야 함. 이를 통해 타입 T
에 대한 함수 정의를 friend
선언 쪽에서 참조 가능.
함수 선언에 사용되는 클래스 템플릿도 전방 선언이 필요.
열심히 글을 작성하시고 질문해주셨다는 점에서 박수 한 번 보내드리고 싶네요. 이런 고민을 많이 해보는 것이 생각하는 힘을 기른다고 생각합니다.
글이 깔끔하지 않더라도 죄송하실 필요는 없어요. 다만 모르는 부분을 물어보시는 질문과는 달리, 공부한 내용을 점검해달라는 유형의 질문에 대해서는 죄송하지만 저도 무척이나 답변드리기 어렵습니다. 저도 최대한 정리해서 답변드리려 했지만, 질문자님 포함 다른 분들이 보시기에는 글이 매끄럽지 않다고 느끼실 수 있습니다. (다루는 내용이 복잡한 것도 한 몫을 했습니다)
공부한 내용에 대해 글로 정리하는 습관을 들이는 것도 강력히 추천드립니다. 스스로가 작성한 글을 알아보는건 쉽지만, 남에게 잘 읽히는 글을 쓰는건 무척이나 어려워요. 이건 저도 아직 잘 하지 못합니다. 글을 많이 쓰다보면 정돈되게 문장을 쓰는 감도 생기고, 생각을 정리하는 과정 자체가 재미있어지는 경험도 하게 되실겁니다.
앞으로도 질문 많이 해주시고, 열심히 공부 하시길 바랍니다. 화이팅입니다. 추가적인 질문이 있다면 달아주세요.
0
안녕하세요, 인프런 AI 인턴입니다.
김기상님께서 템플릿 클래스와 관련된 여러가지 사항을 열심히 복습하신 것 같아 매우 인상적입니다. 질문 내용이 심도 깊고 다층적이라 이를 간단히 요약해 드리겠습니다.
템플릿을 부분적으로 특수화할 때 오버리딩된 함수에 대한 논의
템플릿과 virtual 키워드 사용의 적절성에 대한 논의
이 자료들이 님의 템플릿 사용에 조금이나마 도움이 되길 바라며, 복잡한 문제를 다루실 때 아무쪼록 도움이 되었으면 합니다. 여러 가지 유연한 방법이 존재하기에 상황에 맞게 조합해서 사용하는 것이 중요합니다.
저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다. 현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏 추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.
정말 필요했던 답변 너무나 감사합니다. 생각보다 많은 부분을 오해하고있어서 부끄럽네요ㅜ 앞으로 스스로 점검을 더 해보고, 구체적인 예제와 요점위주로 간결하게 질문드리도록 하겠습니다. 항상 수고많으십니다!