배움 저장소

[홍정모의 따라하며 배우는 C++] 13. 템플릿 본문

Programming Language/C++

[홍정모의 따라하며 배우는 C++] 13. 템플릿

시옷지읏 2021. 12. 30. 16:40

같은 기능을 하지만 다른 자료형으로 동작하는 코드를 작성한다면 여러번 반복작업이 필요하다. 귀찮은 작업을 쉽게 해낼수 있는 템플릿을 사용해보자.

13.1 함수 템플릿


함수 템플릿에 특정 자료형을 사용하면 해당 자료형에 맞는 함수 코드가 만들어진다. 이를 Instantiation이라 표현한다. 자료형에 맞게 만들어진 함수 코드는 Instance라 부른다

//template<class T> // alternatives
template<typename T>
T getMax(T x, T y) {
    return ( x > y ) ? x : y ;
}

int main() {
    std::cout << getMax(1,2) << endl;
    std::cout << getMax(11.1f, 22.2f) << endl;
    std::cout << getMax(111.11, 222.22) << endl;
    std::cout << getMax('a', 'c') << endl;
}

이때 템플릿의 인자는 함수의 인자에 따라 자동으로 결정되었으며 생략가능하다.

getMax<int>(1,2);
getMax<float>(11.1f, 22.2f);
getMax<double>(111.11, 222.22);
getMax<char>('a', 'c');

 

함수 템플릿에 사용자 정의 자료형을 사용할 수 있다

 

13.2 클래스 템플릿


 아래 예제는 int 자료형을 다루는 배열 클래스이다. 아래 클래스에 템플릿을 사용하여 다양한 자료형에 사용가능하도록 바꾸어보자.

 

 myArray.h

class myArray {
private:
    unsigned m_length = 0;
    int* m_data = nullptr;
public:
    myArray(unsigned length=0) :m_length(length) { m_data = new int[length]; }
    ~myArray() { delete[] this->m_data; }
    
    void reset() {
        delete[] m_data;
        m_data = nullptr;
        m_length = 0;
    }
    int& operator[](int index) {
        assert( 0 <= index && index < m_length);
        return m_data[index];
    }
    int getLength(){ return m_length; }

    friend std::ostream& operator << (std::ostream& out, myArray& arr) {
        for (unsigned i = 0; i < arr.m_length; ++i)
            out << arr.m_data[i] << " ";
        return out;
    }
};

main.cpp

int main() {
    myArray myArr(10);
    for (int i = 0; i < myArr.getLength(); ++i) myArr[i] = i;

    cout << myArr << endl;
}

 

class에 template를 적용한 예제

myArray.h

#include <cassert>
#include <iostream>

template<typename T> // T is type parameter
class myArray {
private:
    unsigned m_length = 0;
    T* m_data = nullptr;
public:
    myArray(unsigned length = 0) :m_length(length) { m_data = new T[length]; }
    ~myArray() { delete[] this->m_data; }

    void reset() {
        delete[] m_data;
        m_data = nullptr;
        m_length = 0;
    }
    T& operator[](int index){
        assert(0 <= index && index < m_length);
        return m_data[index];
    }
    int getLength() { return m_length; }

    friend std::ostream& operator << (std::ostream& out, myArray& arr) {
        for (unsigned i = 0; i < arr.m_length; ++i)
            out << arr.m_data[i] << " ";
        return out;
    }
};

main.cpp

#include <iostream>
#include "myArray.h"
using namespace std;

int main() {
    myArray<char> myArr(10);
    for (int i = 0; i < myArr.getLength(); ++i) myArr[i] = i + 'A';

    cout << myArr << endl;
}

 

이 때 함수의 정의를 동일 파일, 클래스 외부에서 정의한다면 함수 정의 위에 template 매개변수를 설정해주어야 한다.

template<typename T>
T& myArray<T>::operator[] (int index)
{
    assert(0 <= index && index < m_length);
    return m_data[index];
}

 

만약 함수의 정의를 코드파일에, 선언을 헤더파일에 했다면 에러가 발생한다

- 코드파일에서 템플릿에서 typename을 구체적으로 정해주어야 하지만 main과 해당 코드파일의 연결고리가 없으므로 typname은 정해지지 않는다. 이 때문에 Linking 에러가 발생한다. main에서 해당 코드파일을 포함시키면 문제를 해결해주지만 권하지 않는다. 큰 프로젝트에서 코드파일을 포함시키면 큰 문제가 발생할 수 있다!

 

Explicit Instantiation

- 코드파일에서 템플릿 함수를 정의했을 때 에러가 발생한다. explicit instantiation을 사용하면 해결할 수 있다.

- explicit instantiation은 템플릿의 자료형을 프로그래머가 정하여 instantiation한다.

- template 클래스에 적용할 수 있다. 각 멤버함수에 한정하여 적용할 수도 있다.

myArray.cpp

#include "myArray.h"

template<typename T>
T& myArray<T>::operator[] (int index)
{
    assert(0 <= index && index < m_length);
    return m_data[index];
}

// explicit instantiation for member fuction
template char& myArray<char>::operator[] (int index);
template double& myArray<double>::operator[] (int index);

// explicit instantiation for class
template class myArray<char>;
template class myArray<double>;

 

13.3 자료형이 아닌 템플릿 매개변수

Non-type Parameters


템플릿 매개변수로 값을 사용할 수 있다. 배열의 크기처럼 특정 값을 템플릿에서 정할 때 사용한다. 이때 상수만 템플릿의 인자로 사용할 수 있다. 상수가 아닌 값이 템플릿의 인자로 사용되면 에러가 발생한다.

template<typename T, unsigned int T_SIZE> // T is type parameter
class myArray {
private:
    T* m_data = nullptr; // heap is huge
    //T m_data[T_SIZE]; // stack is small
public:
    myArray(){ m_data = new T[T_SIZE]; }
    ~myArray() { delete[] this->m_data; }

    void reset() {
        delete[] m_data;
        m_data = nullptr;
    }
    T& operator[](int index){
        assert(0 <= index && index < T_SIZE);
        return m_data[index];
    }
    int getLength() { return T_SIZE; }

    friend std::ostream& operator << (std::ostream& out, myArray& arr) {
        for (unsigned i = 0; i < T_SIZE; ++i)
            out << arr.m_data[i] << " ";
        return out;
    }
};

T_SIZE는 컴파일 타임에 결정되므로 T m_data[T_SIZE]처럼 스택을 활용할 수도 있다. 스택은 힙보다 크기가 작으므로 용도에 맞게 사용하자.

int main() {
    myArray<char, 20> myArr;// argument is const
    for (int i = 0; i < myArr.getLength(); ++i) myArr[i] = i + 'A';

    cout << myArr << endl;

    const int const_length = 10;
    myArray<char, const_length > OK;

    int length = 10;
    myArray<char, length> Errored; // Error!
}

 

Non-type Parameter와 explicit Intantiation을 함께 사용하면 비효율적이다

- 클래스의 멤버함수를 헤더 파일에서 선언하고 코드 파일에서 구현할 때 코드 파일에서 템플릿 매개변수 값은 main과 연결되지 않는다. explicit Instantiation을 선언해주어야 한다

- Non-type Parameter와 explicit Intantiation을 함께 사용해보자

- 위 예제에 적용하면 다음과 같이 작성해야한다. 번거롭다. Non-type과 함께 사용한다면 헤더파일에서 정의해주자

// explicit instantiation for member fuction
template char& myArray<char, 1>::operator[] (int index);
template char& myArray<char, 2>::operator[] (int index);
template char& myArray<char, 3>::operator[] (int index);

// explicit instantiation for class
template class myArray<char, 1>;
template class myArray<char, 2>;
template class myArray<char, 3>;

 

13.4 함수 템플릿 특수화

Specialization Templatize


특정 자료형을 사용했을 때만 다른 기능으로 구현하고 싶다면 함수 템플릿을 특수화하자

 

함수 템플릿의 특수화

- 함수 템플릿이 구현된 예제이다. 문자 자료형을 사용할 때만 다른 코드를 작동하고 싶다면 아래를 참고하자.

template<typename T>
const T& getMax(const T& left, const T& right) {
    return (left < right) ? right : left;
}

template<>
//const char& getMax<char>(const char& left, const char& right) {
const char& getMax(const char& left, const char& right) {
    cout << "Warning: Comparing two character! ";
    return (left < right) ? right : left;
}

int main() {
    cout << getMax(1, 2) << endl;
    cout << getMax('A', 'B') << endl;
}
>> 2
>> Warning: Comparing two character! B

 

멤버 함수 템플릿의 특수화

- 템플릿 클래스 내부에서 멤버 함수의 특수화를 구현할 수 없다. 템플릿 외부에서 정의하자.

 

 Storage.h

#pragma once
#include <iostream>

template<class T>
class Storage {
private:
    T m_value;
public:
    Storage(T value) { m_value = value; }
    ~Storage(){}
    void print(){ std::cout << m_value << std::endl; }
};

template<>
void Storage<double>::print() {
    std::cout << "Scientific noataion: ";
    std::cout << std::scientific << m_value << std::endl;
}

 main.cpp

#include "Storage.h"

int main() {
    Storage<int> iS(5);
    Storage<double> dS(5.5);

    iS.print();
    dS.print();
}
>> 5
>> Scientific noataion: 5.500000e+00

 

13.5 클래스 템플릿 특수화


클래스를 새로 구현하는 것과 차이가 없지만 사용자는 편리하다. 클래스를 일반화하여 사용할 수 있다는 장점이 있다. 

#include <iostream>
using namespace std;

template<typename T>
class A {
public:
    void Function() { cout << typeid(T).name() << endl; }
    void test(){}
};

template<>
class A<char> {
public:
    void Function(){ cout << "Char type specialization" << endl; }
};

int main() {
    A<int>    a_int; a_int.Function();
    A<double> a_double; a_double.Function();
    A<char>   a_char; a_char.Function();
}
>> int
>> double
>> Char type specialization

이 때 A<char> 자료형으로 선언된 a_char은 test 멤버 함수를 호출할 수 없다. 구현되어있지 않다.

 

템플릿 자료형은 생략가능하다

- C++17부터 지원한다.

- 생성자의 매개변수 자료형에 따라 템플릿 자료형이 정해진다. 템플릿 자료형을 생략할 수 있다.

template<typename T>
class A {
public:
    A(const T& t){}
};

template<>
class A<char> {
public:
    A(const char& c){}
};

int main() {
    A<int>    a_int(1);
    A         a_int2(1);

    A<double> a_double(1.1);
    A         a_double2(1.1);
 
    A<char>   a_char('a');
    A         a_char2('a');
}

 

템플릿 특수화 활용예제

- bool 자료형은 1bit만 사용해도 되지만 컴퓨터가 자료를 주고받는 단위는 1byte이기에 7bit가 낭비된다.

- 클래스 템플릿을 작성하되 bool 자료형에만 비트마스크를 사용해보자.

 

 Storage8.h

#pragma once
#include <iostream>

template<class T>
class Storage8 {
private:
    T m_array[8];
public:
    void set(int index, const T& value) {
        m_array[index] = value;
    }

    const T& get(int index) {
        return m_array[index];
    }
};

template<>
class Storage8<bool> {
private:
public:
    unsigned char m_data;
    Storage8():m_data(0){}
    void set(int index, const bool& value) {
        if (value) {
            m_data |= (1<<index);
        }
        else {
            m_data &= (1<<index);
        }
    }
    const bool& get(int index) {
        return m_data >> index;
    }
};

main.cpp

#include "Storage8.h"
#include <iostream>
#include <bitset>
using namespace std;

int main() {
    Storage8<bool> test;

    for (int i = 0; i < 8; ++i) {
        test.set(i, true);
        cout << bitset<8>(test.m_data) << endl;
    }
    cout << "size:" << sizeof(test) << "byte\n";
}
>> 00000001
>> 00000011
>> 00000111
>> 00001111
>> 00011111
>> 00111111
>> 01111111
>> 11111111
>> size:1byte

 

13.6 템플릿을 부분적으로 특수화하기

Partial Specialization Templatize


예제에 사용할 클래스의 정의이다

- 템플릿 함수를 특수화할 때 아래 클래스를 사용한다.

- 아래 클래스는 템플릿 함수의 매개변수로 사용된다.

template <class T, int SIZE>
class Staticarr {
private:
    T m_arr[SIZE];
public:
    T* getarr() { return m_arr; }
    T& operator[](int index) { return m_arr[index]; }
};

 

print함수는 템플릿 매개변수로 자료형과 배열 크기를 받는다. 이 때 자료형만 특수화할 수 있다

- 문자배열을 출력할 때는 입력값 사이에 space가 필요없으므로 문자 배열을 출력하는 경우만 특수화 해보자

template<typename T, int SIZE>
void print(Staticarr<T,SIZE> &arr){
    for (unsigned int i = 0; i < SIZE; ++i)
        cout << arr[i] << " ";
    cout << endl;
}

// specification for char, size is maintained
template<int SIZE>
void print(Staticarr<char, SIZE>& arr) {
    for (unsigned int i = 0; i < SIZE; ++i)
        cout << arr[i]; // No needs space!
    cout << endl;
}
int main() {
    Staticarr<int, 4> int4;
    int4[0] = 0;
    int4[1] = 10;
    int4[2] = 20;
    int4[3] = 30;
    print(int4);

    Staticarr<char, 14> char14;
    strcpy_s(char14.getarr(), 14, "Hello, World");
    print(char14);
}

 

템플릿 클래스 내부에서 특정 멤버함수만 특수화 할 수 없다

- 특수화된 멤버함수를 클래스 외부에서 구현하려면 템플릿의 매개변수 모두를 특수화해주어야 한다. 

- 매개변수의 "부분 특수화"는 허용되지 않는다.

 

템플릿 클래스에서 특정 멤버함수만 특수화하기

(1) Base클래스를 구현하고 일반화된 자료형과 특수화할 자료형을 구분하여 상속하자.

(2) 특수화하고 싶은 멤버함수만 오버라이딩하면 된다.

template <class T, int SIZE>
class StaticArrBASE {
private:
    T m_arr[SIZE];
public:
    T* getarr() { return m_arr; }
    T& operator[](int index) { return m_arr[index]; }
    
    virtual void print() {
        for (unsigned int i = 0; i < SIZE; ++i)
            cout << (*this)[i] << " ";
        cout << endl;
    }
};

template <class T, int SIZE>
class StaticArr : public StaticArrBASE<T, SIZE> {};

template <int SIZE>
class StaticArr<char,SIZE> : public StaticArrBASE<char, SIZE> {
public:
    void print() override {
        for (unsigned int i = 0; i < SIZE; ++i)
            cout << (*this)[i];
        cout << endl;
    }
};

 

13.7 포인터에 대한 템플릿 특수화


템플릿 자료형이 포인터일 때 특수화하기

template<class T>
class A {
private:
    T m_value;
public:
    A(const T& t) : m_value(t) {}
    void print() { cout << m_value << endl; }
};

template<class T>
class A<T*> {
private:
public:
    T* m_ptr;
    A(T* t) : m_ptr(t) {}
    void print() { cout << *m_ptr << endl; }
};

 

 

부분 특수화를 구현할 때는 상속을 이용하자

 

참고) 일반 포인터 변수에 const 포인터를 대입하면 에러가 발생한다

    int a = 2;
    const int* c_ptr = &a;
    int* ptr = c_ptr;  // 에러! 불가능한 표현

 

13.8 멤버 함수를 한 번 더 템플릿화 하기


클래스가 템플릿화 되었을 때 멤버함수에 추가적인 템플릿화를 구현 할 수 있다

template<class T>
class A {
private:
    T m_value;
public:
    A(const T& t): m_value(t){}

    template<typename funcT> // template only for function()
    void function(const funcT& val) {
        cout << typeid(T).name() << " " << typeid(funcT).name() << endl;
    }

    void print(){ cout << m_value << endl; }
};

이 때 매개변수 자료형으로 템플린 매개변수를 확인할 수 있는 경우에는 템플릿 인자를 생략할 수 있다

A<int> a_int(1);
a_int.print();                 // >> 1

a_int.function<float>(1.23f); // >> int float
a_int.function(1.23f);        // >> int float

 

큰 자료형을 작은 자료형에 담아 데이터가 손실되는 문제가 발생할 수 있다

a_int.function<float>(1.23);  // truncation from 'double' to 'const funcT

 

템플릿 클래스와 템플릿 클래스의 템플릿 멤버함수 활용하기

- 템플릿 클래스의 자료형에서 멤버함수 템플릿의 자료형으로 형변환할 수 있다

template<typename funcT>
void function(const funcT& val) {
    cout << "transforming from " << typeid(T).name() << " to " << typeid(funcT).name();
    cout << " is " << funcT(m_value) << endl;
}
int main() {
    A<int> a_int(1);
    a_int.function<float>(1.23f);    // >> Transforming from int to float is 1
    a_int.function(double());        // >> Transforming from int to double is 1
    a_int.function<float>(double()); // >> Transforming from int to float is 1
}
Comments