C++ - Module 07

이호용·2021년 12월 11일
0

cpp

목록 보기
8/16

템플릿

함수나 클래스를 개별적으로 다시 작성하지 않아도, 여러 자료 형으로 사용할 수 있도록 하게 만들어 놓은 틀.

c++에서 템플릿은 두가지로 나눌 수 있다.
함수 템플릿, 클래스 템플릿.

함수 템플릿

함수를 만들어 낼때, 함수의 기능은 명확하지만, 자료형을 모호하게 둡니다.

기존의 오버라이딩 방법으로 다양한 자료형을 처리할려면

int sum(int a, int b){
   return a + b;
}
double sum(double a, double b){
   return a + b;
}

위와 같은 방법으로 자료형 마다 함수를 정의해주었다. 그러나 이러한 방법은 시간도 걸리고 함수 내용이 같은데 자료형 때문에 바꿔준다는게 너무 비효율적이다. 이를 해결해주는 기능이 template이다.

template <typename T>
T sum(T a){
   return a;
}

or (템플릿은 한개이지만 매게변수는 두개도 되고 더 많아도 된다.)

template <typename T>
T sum(T a, T b){
    return a + b;
}

이렇게 템플릿을 이용하면, 자료형 마다 따로 선언하지 않아도 모든 자료형을 받을 수 있다.

메게변수 자료형이 서로 다를때.

만약 메게변수 자료형 종류가 2개 이상일때 자료형이 서로 다르다면??

sum(1, 1.0); // 앞에껀 int형이고 뒤에껀 double이다. 

위와 같이 코드를 돌려보았다.

test.cpp:12:15: error: no matching function for call to 'sum'

template <typename T>
T sum(T a, T b){
    return a + b;
}

분명 T sum(T a, T b)의 두개의 매게변수를 받는 함수가 있는데, 매칭되지 않는다는 에러가 나온다. 자료형의 종류가 두개이므로 우리는 template 도 두개를 받을 수 있도록 해주어야한다.

template <typename T1, typename T2>
T1 printAll(T1 a, T2 b){
	return a + (T1)b;
}

이런식으로 메게변수의 typename T2도 추가해주고난뒤 에러가 나지 않는다.

그렇다면 세개일때는?

매게변수 종류 2개 메게변수 3개일때

  3 template <typename T, typename T2>
  4 T sum(T a, T2 b, T c){
  5     std::cout << sizeof(T2) << std::endl;
  6     return a + b + c;
  7 }
int test1 = 1;
double test2 = 2;
int test3 = 1;
  
sum(test1, test2, test3); // 앞에껀 int형이고 뒤에껀 double이다. 

코드를 보자. 메게변수는 3개이지만, int와 double형 밖에 없으므로 <typename T, typename T2>로 정의하니 sum(test1, test2, test3);이 잘 매칭되어 실행되었다.

template <typename T, typename T2> 자료형 받는 순서

자 하나만 더 테스트 해보자!

 3 template <typename T, typename T2>
 4 T sum(T a, T b, T2 c){
 5     std::cout << sizeof(T2) << std::endl;
 6     return a + b + c;
 7 }
11     int test1 = 1;
12     int test2 = 1;
13     double test3 = 1;
14
15     std::cout << sum(test1, test2, test3) << std::endl;
16

자 만약 test2가 double이 아니라 test3가 double라면??

이것도 똑같이 T2는 double형이 되었다. typename개수가 매게변수 개수보다 적으면 종류순으로 받고 typename개수가 같으면 순차적으로 받는거 같다.

정확하게 받는 순서는 모르겠다. 단지 추측되는건 받은 typename를 sum에 어떤식으로 넣어주냐에 따라 template가 받는 순서도 달라진다.

템플릿 특수화!

함수 템플릿을 사용하면 어떠한 자료형이 들어오든 하나의 함수로 처리할수 있다. 그러나, 가끔 특정 자료형이 들어왔을때 따로 처리하고 싶을때도 있다. 이런 상황에서 함수템플릿 특수화는 매우 유용하다.

sum(1, 1); // 앞에껀 int형이고 뒤에껀 double이다. 
template <typename T>
T sum(T a, T b){
    return a + b;
}
  
template <> //템플릿 특수화
T sum(int a, int b){
  return a + b;
  }

이렇게 사용하면 int형의 자료형이 들어오는건

T sum(int a, int b)

에서 작동되고 int을 제외한 자료형이 들어오면

T sum(T a, T b)

에서 처리한다.

클래스 템플릿.

클래스 템플릿(class template)도 마찬가지로 클래스의 일반화이다. 클래스 템플릿을 정의하면 타입에 따라 클래스를 생성할 수 있다.

함수 템플릿과 다른 점이 있다 함수의 경우 명시적으로 템플릿 인수를 작성하지 않아도 동작했지만 클래스 템플릿은 무조건 템플릿 인수를 명시해 주어야 한다.

test<int> tmp; //<int>이런식으로 템플릿에 들어갈 자료형을 명시해주어야함.

이유는 클래스의 객체를 생성하는 과정에 있다. 인스턴스화를 진행할 때 해당 객체에 대한 메모리를 할당하고 생성자를 호출하게 된다. 하지만 클래스 템플릿의 데이터 타입이 결정되려면 생성자가 호출되어야 한다. 따라서 명시적으로 템플릿 인수를 작성하지 않을 경우 어떤 타입에 대한 메모리를 할당해야 하는지 모르기 때문에 객체를 생성할 수 없기 때문에 객체 생성 시 템플릿 인수를 명시해 주어야 한다.

클래스 템플릿 사용법

  //main.cpp file
  template <typename T1>
  class aa
  {
      // 클래스 멤버의 선언
    public :
      T1 test;
  };
  
  int main(void)
  {
  	aa<int> aa_1;
  	return 0;
  }
template <typename T1>

클래스 위에 위와 같은 형태로 선언하고 class에서 T1을 자료형으로 사용해주면된다.

대신 사용할때는, 어떠한 자료형을 쓸지 객체를 만들때 넣어주면 된다.

클래스 템플릿 멤버함수 정의

//main.cpp file
#include <iostream>

template <typename T1>
class aa
{
  // 클래스 멤버의 선언
public :
  T1 test;
	void tp_print(T1 data);
};

template <typename T1>
void aa<T1>::tp_print(T1 data)
{
	std::cout << data << std::endl;
}

int main(void)
{
	aa<int> aa_1;
	aa_1.tp_print(2);
	return 0;
}

tp_print를 정의 해보았다. tp_print에서 사용할 T1자료형을 위해 템플릿도 다시 정의해주고 aa의 범위지정자를 사용할때도 어떠한 자료형의 aa인지 명시 해주어야해서

aa<T1>::

이런식으로 사용한다.

### 클래스 템플릿 특수화
//aa.hpp
#ifndef __AA_HPP__
#define __AA_HPP__

template <typename T1>
class aa
{
  // 클래스 멤버의 선언
public :
  T1 test;
	void tp_print(T1 data);
};

template <>
class aa<int>
{
  // 클래스 멤버의 선언
public :
  int test;
	void tp_print(int data);
	int example(char data2);
};

#endif

함수 템플릿 특수화와 비슷하다.
템플릿으로 들어오는 자료형이 int일때만 특별하게 동작하도록 정의해준다.
나머지 자료형들은 기존에 정의한 템플릿 클래스로 들어간다.

클래스 템플릿의 부분 특수화

  template <typename T1, typename T2>
class aa{...};
  
  
  template <> class aa<int, double> {......};

  template <template T1> class aa<T1, double> {......};

클래스 템플릿을 좀 더 세미하게 나누어 줄수 있다.

중첩 클래스 템플릿.

template <typename T>
class X
{
  template <typename U>
  class Y
  {
      ...
  }
  ...
  }

int main(void)
{
  ...
}

template <typename T>
template <typename U>
X<T>::Y<U>::멤버함수이름()
{
  ...
}

클래스 템플릿안에 클래스 템플릿이 있는 경우다.

클래스 템플릿 상속

template <typename T1>
class aa
{
  // 클래스 멤버의 선언
public :
  T1 test;
	void tp_print(T1 data);
};

template <typename U>
class bb : public aa<U> {
	U money;
public:
	Derived(U a, U b) : aa<U>(a) { money = b; }
}

aa라는 클래스 템플릿을 만들었다.
부모 aa의 자식인 bb를 만들었다. bb에 부모 aa의 템플릿 타입을 U로 넣어주면 기존의 aa를 사용할때와 같은 형태로 사용할 수 있다.

만약 자식에서는 템플릿 형태로 쓰고 싶지 않다면 자식을 정의할때 일반화가 아닌 명확히 정의 해주면된다.

class bb : public aa<int> {
	int asset;
public:
	Derived(int a, int b) : aa<int>(a) { asset = b; }
	void showDerived() { cout << asset << endl; }
};

보다 시피 부모 aa를 typename으로 받지않고 그냥 int형으로 확실하게 정하여 받았다. 이렇게 사용하면 main에서 자식을 사용할때 따로 템플릿 자료형을 정의 해줄필요없이 부모의 템플릿 자료형은 int형으로 사용할수 있다.

클래스 템플릿 파일 나누기

매우매우매우 중요하다.

방금전 예제를 main.cpp hoylee.cpp hoylee.hpp로 나누어 보자.

파일 나누기 에러 케이스

클래스 템플릿을 일반 class파일 나누듯이 나누고 컴파일 하면 에러가 난다.

//main.cpp
#include <iostream>
#include "aa.hpp"
int main(void)
{
	aa<int> aa_1;
	aa_1.tp_print(2);
	return 0;
}
//aa.hpp
#ifndef __AA_HPP__
#define __AA_HPP__

template <typename T1>
class aa
{
  // 클래스 멤버의 선언
public :
  T1 test;
	void tp_print(T1 data);
};

#endif
//aa.cpp
#include "aa.hpp"

template <typename T1>
void aa<T1>::tp_print(T1 data)
{
	std::cout << data << std::endl;
}
clang++ main.cpp aa.cpp

아래처럼 main문을 만들때 링크가 되지않는다고 에러가 나온다.

만약 당신이 template를 지우고 일반 클래스로 변경하고 컴파일하면 정상적으로 컴파일 되는걸 확인할수 있을거다.

이렇게 템플릿 클래스만 컴파일 에러가 나는 이유는.

컴파일 과정을 좀더 이해해야한다.

컴파일 진행시 clang++ main.cpp aa.cpp를 진행해도 두파일이 컴파일시 서로 참조하며 컴파일 하지 않는다.

무슨 말이냐면, main에서 바이너리 실행파일로 만들려면 class를 정의한 내용이 필요한데, main.cpp의 내용만 가지고는 클래스가 어떠한 형태로 정의 되었는지 판단할수 없다.

이러한 문제를 해결하기위해 두가지 방법이 있다.

클래스 템플릿 파일나누기 해결책 1

//main.cpp
#include <iostream>
#include "aa.hpp"

int main(void)
{
	aa<int> aa_1;
	aa_1.tp_print(2);
	return 0;
}
//aa.hpp
#ifndef __AA_HPP__
#define __AA_HPP__

template <typename T1>
class aa
{
  // 클래스 멤버의 선언
public :
  T1 test;
	void tp_print(T1 data);
};

#include "aa.tpp"
#endif
//aa.tpp
#include <iostream>

	template <typename T1>
void aa<T1>::tp_print(T1 data)
{
	std::cout << data << std::endl;
}

aa.tpp는 오타가 아니다. 꼭 tpp확장자 아니여도됨.

중요하게 봐야할건. aa.hpp맨 밑에

#include "aa.tpp"

라고 선언하고 aa.tpp파일에서 tp_ptrint를 정의했다.

눈치 빠른 사람은 알겠지만, aa.hpp파일안에서 tp_ptrint파일을 정의 한거랑 같다.
분리를 안하고 aa.hpp파일에 다 정의한거다.

파일을 분리하는 이유는 어디까지나 코드를 관리하기 쉽고 확장성있게 하는것임을 잊지말자.

클래스 템플릿 파일나누기 해결책 2

//main.cpp
#include <iostream>
#include "aa.hpp"
#include "aa.cpp"

int main(void)
{
	aa<int> aa_1;
	aa_1.tp_print(2);
	return 0;
}
#ifndef __AA_HPP__
#define __AA_HPP__

template <typename T1>
class aa
{
  // 클래스 멤버의 선언
public :
  T1 test;
	void tp_print(T1 data);
};

#endif
//aa.cpp
#include <iostream>
#include "aa.hpp"

	template <typename T1>
void aa<T1>::tp_print(T1 data)
{
	std::cout << data << std::endl;
}

main.cpp를 보면

#include "aa.cpp"

aa.cpp파일을 불러왔다. main에서 컴파일할때 aa.cpp파일에 대한 정보를 못찾아서 컴파일 에러가나는것임으로 main을 컴파일 할때 aa.cpp가 포함되어 컴파일 되도록 넣어주는 거다.

참고 :
https://stackoverflow.com/questions/495021/why-can-templates-only-be-implemented-in-the-header-file
https://koey.tistory.com/113
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=vjhh0712v&logNo=221561418596

0개의 댓글