본문 바로가기
전문가를 위한 C++

2025/03/16 ch.12 템플릿으로 제네릭 코드 만들기

by 딴짓거리 2025. 3. 16.

클래스가 베이스 클래스를 상속받은 파생 클래스 뿐만 아니라 기본 타입도 지원하려면 포인터 전달방식이 아닌 값 전달방식을 사용하는것이 유리하다.

값 전달방식은 변수에 항상 어떤 값이 들어있어야 하기 때문에 완전히 빈 곳을 만들기 어렵다. 

포인터 기반으로 구현하면 nullptr로 빈 공간을 만들 수 있다.

하지만 optional을 이용하면 값 전달 방식을 사용하며 완전히 빈 공간을 만들 수 있다.

 

export template <typename T>

//이런식으로 표현도 가능
<class T>

뒤에 나올 클래스 정의가 T로 지정한 특정한 타입에 적용할 수 있는 템플릿이라고 선언

함수가 매개변수를 통해 값을 받는 것처럼 템플릿도 타입을 매개변수로 받는다.

T에는 특별한 의미가 없으며 다른 이름으로 정해도 된다.

 

포인터 기반으로 클래스를 만들면 복제생성자 등등을 직접 작성해줘야 하지만

이렇게 디자인하면 디폴트 생성자로 충분하다

 

//이렇게 작성하면
Grid& operator= (const Grid& rhs) = default;
//컴파일러는 이렇게 처리함
Grid<T>& operator= (const Grid<T>& rhs) = default;

//선언의 예)
Grid<int> grid;

클래스 정의 내에서는 자동으로 <T>로 처리해주지만 클래스 정의 밖에서는 반드시 클래스<T>로 사용해줘야 한다.

Grid가 클래스 이름처럼 보이지만 엄밀히 말해서 Grid는 템플릿 이름임

 

메서드를 정의할때도 반드시 템플릿 지정자를 앞에 적어야됨

 

# 특정한 템플릿 타입 매개변수 T에 대해 디폴트값을 지정하려면 T{} 와 같은 문법에 따라 작성해야 함.

T가 클래스 타입이면 해당 객체의 디폴트 생성자를 호출하고 기본타입이면 0을 생성해줌 = 영초기화

 

템플릿 인스턴스화

클래스 템플릿에 특정한 타입을 지정해서 구체적인 클래스를 만드는 것

Grid<int> myIntGrid; 
Grid<double> my DoubleGrid { 11, 11 };

 

 

12.2.2 컴파일러에서 템플릿을 처리하는 방식

컴파일러는 템플릿 메서드 정의 코드를 발견하면 컴파일하지 않고 문법 검사만 함

템플릿 정의만 보고서는 실제로 어떤 타입으로 사용될지 알 수 없기 때문

 

컴파일러가 템플릿을 인스턴스화 하는 코드를 발견하면 주어진 타입에 대한 인스턴스를 생성함.

복사 붙여넣기와 단어 바꾸기 적업을 자동화한 것

클래스 템플릿 정의 코드는 있는데 특정한 타입에 대한 인스턴스화를 전혀 하지 않으면 그 코드는 컴파일되지 않음

 

 

암묵적인 클래스 템플릿 인스턴스화

int main() {
    MyClass<int> obj1(42);  
    MyClass<double> obj2(3.14); 

    obj1.print();
    obj2.print();
}

일반적으로 이렇게 작성하면 템플릿 클래스에 해당 자료형을 넣어서 컴파일됨. 암묵적인 클래스 템플릿 인스턴스화

 

# int나 double등의 기본 타입을 넣은 이 경우에는 영인수 생성자, 소멸자, 비const 이런 메서드만 컴파일하고

복제 생성자, 대입 연산자 등등에 대한 지금은 쓸데없는 코드들은 생성하지 않는다. 선택적 인스턴스화

 

template class MyClass<int>;  // ✅ MyClass<int>에 대한 인스턴스화만 생성됨
template class MyClass<double>; // ✅ MyClass<double>도 명시적으로 생성

int main() {
    MyClass<int> obj1(42);
    MyClass<double> obj2(3.14);
    obj1.print();
    obj2.print();
}

하지만 이렇게 템플릿에 들어갈 수 있는 타입을 명시해주는 명시적 클래스 템플릿 인스턴스화를 이용하면

템플릿 클래스에 들어갈 수 있는 타입을 명시적으로 제한해줄 수 있음

 

템플릿을 사용할 타입의 요건

타입에 독립적인 코드를 작성하려면 여기에 적용할 타입에 대해 어느 정도 고려해야 한다.

어떤 클래스 템플릿을 인스턴스화할 때 그 템플릿에 있는 연산을 모두 지원하지 않으면 컴파일에러가 발생, 출력되는 에러 메시지도 이해하기 힘듦.

선택적 인스턴스화를 이용하여 특정 메서드만 사용하게 만드는식으로 처리할 수 있다.

 

C++20

콘셉트라는 기능이 추가되었다. 템플릿 매개변수에 대한 요구사항을 컴파일러가 해석하고 검증할 수 있도록 작성할 수 있다.

 

12.2.3 템플릿 코드를 여러 파일로 나누기

컴파일러는 소스 파일을 컴파일하는 과정에서 클래스 템플릿과 메서드를 사용하는 부분이 나올때마다 이에 대한 정의 코드를 반드시 참조해야 함.

이를 위해 C++에서 제공하는 메커니즘

모듈에 대한 내용인거 같아서 스킵

 

# 클래스 템플릿은 헤더와 cpp로 분리하면 안된다. 링킹 오류 발생!!!

 

헤더파일은 애초에 컴파일되는게 아니다. cpp파일만 컴파일된 이후 같은 프로젝트 내의 cpp파일들끼리 링킹되는것

템플릿 클래스는 사용되는 순간에만 인스턴스화되된다. 컴파일러가 특정 타입이 필요할 때 실제 클래스를 생성

따라서 컴파일 타임에 템플릿 정의를 알아야 하므로 cpp에 정의하면 찾을 수 없다!

 

정리하자면 클래스가 컴파일할때는 cpp먼저 컴파일 -> 링킹

cpp 파일끼리는 컴파일이 완료되고 링커가 연결해주기 전까지는 알 수 없음

 

1. 일반적인 클래스

//A.h
class A
{
	int func1();
}
//A.cpp
int A::func() {
	//some function
}
//main.cpp

int main()
{
	A tmp;		//A 타입 tmp를 선언하고
	tmp.func1();//A.cpp가서 func1 기능을 실행하렴
}

일반적으로 클래스와 메서드 함수의 작동방식이다

 cpp에는 헤더파일이 include로 복사되지만 헤더파일은 cpp를 볼 수 없으며

기껏해야 컴파일이 완료된 후 링킹된 후 그 기능을 실행하라는 명령을 남겨놓는것 뿐이다

이 때는 A.cpp는 완전한 코드이므로 컴파일 타임에 정상적으로 컴파일되고 링커에서 참조가능하다

 

//B.h
template<typename T>
class B
{
	int func2();
}
//B.cpp
int B::func2()
{
	//using T some function
}

 

//main.cpp

int main()
{
	B<int> tmp;		//B 타입<int> tmp를 선언한다
}

 

 

1. 헤더랑 cpp를 나눠버린 경우

템플릿으로 구현된 클래스는 사용되기전에 컴파일되지 않는다

즉 B.cpp는 main이 참견할 방법이 없으므로 컴파일되지 않고 죽어버린다

B<int> 타입으로 tmp를 선언하고 사용하면 컴파일러는 B<int>를 B.cpp에서 찾아오라고 남겨놓겠지만

B<int>는 컴파일된 적이 없으므로 링커에서 에러가 나는것

 

2. 헤더에 전부 잘 정의한 경우

컴파일 할때 헤더에서 들어온 코드들에서 T를 int로 교체한 후 int로 찍어낸 클래스를 컴파일 완료함

 

~637p