Programming Language/C++

[C++] 함수 템플릿과 클래스 템플릿

lumana 2024. 6. 28. 19:41

함수 템플릿과 클래스 템플릿

함수 템플릿

  • 배열의 합을 구하는 예제를 봐보자
#include <iostream>
using namespace std;

int getArraySum(const int arr[], int n) {
    int sum = 0;
    for (int i = 0; i < n; i++) {
        sum += arr[i];
    }
    return sum;
}

int main() {
    int arr[5] = { 3, 1, 4, 1, 5 };
    int sum = getArraySum(arr, 5);
    cout << sum << endl;
}
  • int 형이 아닌 float 타입등 여러 자료형으로 이루어진 배열의 합을 구하고 싶은 경우가 있을 것이다.

    • 이 때 Overloading을 통해서 float getArraySum이라는 함수를 또 만들어 줘야 한다
#include <iostream>
using namespace std;

int getArraySum(const int arr[], int n) {
    int sum = 0;
    for (int i = 0; i < n; i++) {
        sum += arr[i];
    }
    return sum;
}

float getArraySum(const float arr[], int n) {
    float sum = 0;
    for (int i = 0; i < n; i++) {
        sum += arr[i];
    }
    return sum;
}

int main() {
    int arr[5] = { 3, 1, 4, 1, 5 };
    float farr[5] = { 3.1, 1.2, 4.3, 1.4, 5.5 };
    float sum = getArraySum(farr, 5);
    cout << sum << endl;
}
  • float 자료형 이외에도 다른 자료형의 합을 구할 경우가 생길 수 있는데, 이 때 자료형 부분만 다르고 나머지는 공통적이므로, 자료형을 T라고 치환해서 처리하면 깔끔할 것 같다
T getArraySum(const T arr[], int n) {
    T sum = 0;
    for (int i = 0; i < n; i++) {
        sum += arr[i];
    }
    return sum;
}
  • 위와 같은 기능을 해주는 것이 함수 템플릿이다.
#include <iostream>
using namespace std;

template<typename T>
T getArraySum(const T arr[], int n) {
    T sum = 0;
    for (int i = 0; i < n; i++) {
        sum += arr[i];
    }
    return sum;
}

int main() {
    int iarr[5] = {3, 1, 4, 1, 5};
    float farr[5] = {3.1, 1.2, 4.3, 1.4, 5.5};
    int isum = getArraySum<int>(iarr, 5);
    float fsum = getArraySum<float>(farr, 5);
    cout << isum << endl;
    cout << fsum << endl;
}
  • 임의의 타입에 대한 함수를 찍어낼 수 있다.

  • typename T : 타입 파라미터

  • getArraySum : int 버전의 getArraySum을 만들어라

  • 템플릿은 템플릿 자체로 함수가 아니다. 함수를 생성해주는 템플릿이다.

  • 템플릿 하나만 만들어 준다면 임의의 타입에 대해서도 함수를 생성해준다

Example) Vector 클래스

#include <iostream>
using namespace std;

class Vector2 {
   public:
    Vector2() : x(0), y(0) {}
    Vector2(float x, float y) : x(x), y(y) {}
    float GetX() const { return x; }
    float GetY() const { return y; }

   private:
    float x, y;
};

template <typename T>
T getArraySum(const T arr[], int n) {
    T sum = 0;
    for (int i = 0; i < n; i++) {
        sum += arr[i];
    }
    return sum;
}

int main() {
    int iarr[5] = {3, 1, 4, 1, 5};
    float farr[5] = {3.1, 1.2, 4.3, 1.4, 5.5};
    Vector2 varr[3] = { Vector2(1, 2), Vector2(3,4), Vector2(5, 6) };

    int isum = getArraySum<int>(iarr, 5);
    float fsum = getArraySum<float>(farr, 5);
    Vector2 vsum = getArraySum<Vector2>(varr, 5);

    cout << isum << endl;
    cout << fsum << endl;
}
  • 컴파일 타임에 에러가 발생한다

    • 함수를 만들려고 할 때 T sum = 0에서 0을 Vector2 타입으로 변환하는 과정에서 오류가 발생한다
  • 해결책 1

    • T sum = arr[0];으로 하고 for문을 i = 1 부터 돌려보자
template <typename T>
T getArraySum(const T arr[], int n) {
    T sum = arr[0];
    for (int i = 1; i < n; i++) {
        sum += arr[i];
    }
    return sum;
}
  • 이렇게 해도 오류가 발생한다

    • Vector2 클래스의 += 연산자에 대한 정의가 되어있지 않아 오류가 발생한다.

    • 연산자 오버로딩을 해줘야 한다

class Vector2 {
   public:
    Vector2() : x(0), y(0) {}
    Vector2(float x, float y) : x(x), y(y) {}
    float GetX() const { return x; }
    float GetY() const { return y; }

    Vector2 &operator+=(const Vector2 &rhs) {
        x += rhs.x;
        y += rhs.y;
        return *this;
    }

   private:
    float x, y;
};

int main() {
    int iarr[5] = {3, 1, 4, 1, 5};
    float farr[5] = {3.1, 1.2, 4.3, 1.4, 5.5};
    Vector2 varr[3] = { Vector2(1, 2), Vector2(3,4), Vector2(5, 6) };

    int isum = getArraySum<int>(iarr, 5);
        // getArraySum(iarr, 5); 도 가능하다
    float fsum = getArraySum<float>(farr, 5);
        // getArraySum(farr, 5); 도 가능하다
    Vector2 vsum = getArraySum<Vector2>(varr, 5);
        // getArraySum(varr, 5); 도 가능하다
    cout << isum << endl;
    cout << fsum << endl;
    cout << vsum.GetX() << ", " << vsum.GetY() << endl;
}
  • getArraySum(iarr, 5)에서 굳이 를 안밝혀주고 getArraySum(iarr, 5)로 적어도 컴파일러가 알아서 타입을 추정해준다

    • 즉, 타입 인수를 생략해도 된다
  • string 타입 배열에 대해서도 작동한다

int main() {
    int iarr[5] = {3, 1, 4, 1, 5};
    float farr[5] = {3.1, 1.2, 4.3, 1.4, 5.5};
    Vector2 varr[3] = { Vector2(1, 2), Vector2(3,4), Vector2(5, 6) };
    string sarr[3] = { "hello", "world", "luna" };

    int isum = getArraySum<int>(iarr, 5);
    float fsum = getArraySum<float>(farr, 5);
    Vector2 vsum = getArraySum<Vector2>(varr, 5);
    string ssum = getArraySum(sarr, 3);

    cout << isum << endl;
    cout << fsum << endl;
    cout << vsum.GetX() << ", " << vsum.GetY() << endl;
    cout << ssum << endl;
}
  • 위에서 T sum = arr[0]로 바꿨던 해결책 1 말고도 다른 방법이 존재한다. 아래 코드를 봐보자
template <typename T>
T getArraySum(const T arr[], int n) {
    T sum = new T();
        // T sum;은 불가능
    for (int i = 1; i < n; i++) {
        sum += arr[i];
    }
    return sum;
}
  • T의 기본 생성자를 호출한다.

    • int나 float의 경우 0으로 초기화되고, 사용자 정의 클래스에서는 각 클래스에서 정의한 기본 생성자가 호출된다

    • string의 경우 빈 문자열이 생긴다

  • T sum; 요거는 불가능 하다

클래스 템플릿

Example) Vector 클래스

#include <iostream>
#include <string>
using namespace std;

class Vector2 {
   public:
    Vector2() : x(0), y(0) {}
    Vector2(float x, float y) : x(x), y(y) {}
    float GetX() const { return x; }
    float GetY() const { return y; }

    Vector2 operator+(const Vector2 &rhs) const {
        return Vector2(x + rhs.x, y + rhs.y);
    }

    Vector2 operator-(const Vector2 &rhs) const {
        return Vector2(x - rhs.x, y - rhs.y);
    }

    Vector2 &operator+=(const Vector2 &rhs) {
        x += rhs.x;
        y += rhs.y;
        return *this;
    }

    Vector2 &operator-=(const Vector2 &rhs) {
        x -= rhs.x;
        y -= rhs.y;
        return *this;
    }

   private:
    float x, y;
};

int main() {
    Vector2 v1(2, 3);
}
  • Vector2 클래스에서 float 말고도 int, double 등등의 타입을 담을 수 있게 만들고 싶다.

    • Vector2d, Vector2i와 같이 별도로 클래스를 만드는 것은 매우 번거로움
  • 클래스 템플릿이라는 것을 사용하면 쉽게 구현할 수 있다.

#include <iostream>
#include <string>
using namespace std;

template <typename T>
class Vector2 {
   public:
    Vector2() : x(0), y(0) {}
    Vector2(T x, T y) : x(x), y(y) {}
    T GetX() const { return x; }
    T GetY() const { return y; }

    Vector2 operator+(const Vector2 &rhs) const {
        return Vector2(x + rhs.x, y + rhs.y);
    }

    Vector2 operator-(const Vector2 &rhs) const {
        return Vector2(x - rhs.x, y - rhs.y);
    }

    Vector2 &operator+=(const Vector2 &rhs) {
        x += rhs.x;
        y += rhs.y;
        return *this;
    }

    Vector2 &operator-=(const Vector2 &rhs) {
        x -= rhs.x;
        y -= rhs.y;
        return *this;
    }

   private:
    T x, y;
};

int main() {
    Vector2<float> v1(2, 3);
    Vector2<double> v2(4, 5);
}
  • Tip)

    • Mac의 경우 vscode에서 커맨드 + Shift + L을 통해서 한 번에 바꾸기를 할 수 있다.

      • 윈도우의 경우 아마 ctrl + h로 가능했던 것 같다.
  • Vector2와 Vector2은 다른 타입이다.

    • Vector2 자체는 클래스가 아니라 클래스를 만들어내는 템플릿이기 때문
  • Vector2는 8바이트 크기를 갖는다는 것을 알 수 있고, Vector2은 16바이트 크기를 갖는다는 것을 알 수 있다.

  • 주의! 클래스 템플릿에서는 템플릿 인수를 생략할 수 없다.

질문) Vector2는 클래스가 아니라 클래스 템플릿인데, 연산자 오버로딩에서 클래스를 어떻게 다루는거죠?

template <typename T>
class Vector2 {
   public:
    /* 생략 */
    Vector2 operator+(const Vector2 &rhs) const {
        return Vector2(x + rhs.x, y + rhs.y);
    }

    Vector2 operator-(const Vector2 &rhs) const {
        return Vector2(x - rhs.x, y - rhs.y);
    }

    Vector2 &operator+=(const Vector2 &rhs) {
        x += rhs.x;
        y += rhs.y;
        return *this;
    }

    Vector2 &operator-=(const Vector2 &rhs) {
        x -= rhs.x;
        y -= rhs.y;
        return *this;
    }

   private:
    T x, y;
};
  • 여기서 Vector2를 클래스 템플릿이 아니라 클래스인 것 마냥 사용하고 있다.

    • 이게 어떻게 가능하냐?

    • Vector2에서 가 생략되어 있는 것이다.

Vector2<T> operator+(const Vector2<T> &rhs) const {
        return Vector2(x + rhs.x, y + rhs.y);
}
  • 그리고 템플릿 인자를 2개 이상도 사용할 수 있다.
template <typename T, typename U>
void f(T a, U b) {}

참조) 두들낙서 C/C++ 강좌