C++ 공부 - 모두의 코드(4)

자훈·2023년 11월 22일
0

C++ / C study

목록 보기
5/8
post-thumbnail

📌 Template

☀️ Template 기본 개념

왠만하면 직관적인 언어로 기능을 암시하는 c++이기에, 단어의 의미를 잘 알고 있으면 좋다. Template는 본뜨는 공구. 형판. 이라는 뜻이 있다. 즉 내가 어떤 재료를 주면 그걸 바탕으로 찍어내는 기능을 아마도 행해주고 있을 것이다.

모두의 코드에서는 Template에 대한 예시 코드로 간단하게 만든 vector 클래스로 설명을 도와주고 있다.

#include <iostream>
#include <string>

template <typename T>
class Vector {
  T* data;
  int capacity;
  int length;

 public:
  // 생성자
  Vector(int n = 1) : data(new T[n]), capacity(n), length(0) {}

  // 맨 뒤에 새로운 원소를 추가한다.
  void push_back(T s) {
    if (capacity <= length) {
      T* temp = new T[capacity * 2];
      for (int i = 0; i < length; i++) {
        temp[i] = data[i];
      }
      delete[] data;
      data = temp;
      capacity *= 2;
    }

    data[length] = s;
    length++;
  }

  // 임의의 위치의 원소에 접근한다.
  T operator[](int i) { return data[i]; }

  // x 번째 위치한 원소를 제거한다.
  void remove(int x) {
    for (int i = x + 1; i < length; i++) {
      data[i - 1] = data[i];
    }
    length--;
  }

  // 현재 벡터의 크기를 구한다.
  int size() { return length; }

  ~Vector() {
    if (data) {
      delete[] data;
    }
  }
};

int main() {
  // int 를 보관하는 벡터를 만든다.
  Vector<int> int_vec;
  int_vec.push_back(3);
  int_vec.push_back(2);

  std::cout << "-------- int vector ----------" << std::endl;
  std::cout << "첫번째 원소 : " << int_vec[0] << std::endl;
  std::cout << "두번째 원소 : " << int_vec[1] << std::endl;

  Vector<std::string> str_vec;
  str_vec.push_back("hello");
  str_vec.push_back("world");
  std::cout << "-------- std::string vector -------" << std::endl;
  std::cout << "첫번째 원소 : " << str_vec[0] << std::endl;
  std::cout << "두번째 원소 : " << str_vec[1] << std::endl;
}

Template를 사용하지 않았다면

class Vector {
  int* data;
  int capacity;
  int length;

자료형 포인터 변수 data에 대해 똑같은 코드를 계속해서 만들어가야 한다.
int, std::string, char, longlong 등
그렇기에 C++에서 제공하는 Template기능은 사용자가 원하는 자료형으로 코드를 찍어내도록 만들어주는 기능을 말할 것이다.

사용 방법은 간단하다.
<> 안에 자료형을 넣어주기만 하면 된다.

Vector<int> int_vec;

이것을 통해 템플릿 인자에 타입을 전달해줄 수 있게 되었다. 지금까지의 객체나 인자들에서 타입을 전달한 적은 없다. 그러나 템플릿을 통해 가능!!

클래스 템플릿에 인자를 전달해서 실제 코드를 생성하는 것을 클래스 템플릿 인스턴스화 (class template instantiation) !!!

🌕 Template Specialization

템플릿 특수화는 특정한 데이터 타입이나 조건에 대해 특별한 동작을 정의하는 기능을 말한다. 그렇게하면서 나머지 일반적인 기능은 유지할 수 있는 장점이 있다. 아래 예시코드를 보자.

#include <iostream>

// 일반적인 템플릿 선언
template <typename T>
class Printer {
public:
    void print(T value) {
        std::cout << "Generic Printer: " << value << std::endl;
    }
};

// int 타입에 대한 템플릿 특수화
template <>
class Printer<int> {
public:
    void print(int value) {
        std::cout << "Specialized Printer for int: " << value << std::endl;
    }
};

// double 타입에 대한 템플릿 특수화
template <>
class Printer<double> {
public:
    void print(double value) {
        std::cout << "Specialized Printer for double: " << value << std::endl;
    }
};

int main() {
    Printer<char> charPrinter;
    charPrinter.print('A');  
    Printer<int> intPrinter;
    intPrinter.print(42);    

    Printer<double> doublePrinter;
    doublePrinter.print(3.14);  

    return 0;
}

int 와 double 자료형에 대해서는 특수화가 이루어져있기에 출력을 하게된다면 아래처럼 나올 것이다.

Generic Printer: A
Specialized Printer for int: 42
Specialized Printer for double: 3.14

특수화를 할 때, template 인자를 전달해주지 않더라도, template<>는 작성해야하며, template를 여러개로 선언하여 사용할 수도 있다는 것을 참고하면 도움이 된다.

주의사항?!

template <typename T> = template<class T> 로 정확히 의미가 같지만, typename을 권장. class라고 쓰면 class만 와야하는 것 같으니까.

🔎 Function Template

함수에서도 template 기능을 사용할 수 있다.

#include <iostream>
#include <string>

template <typename T>
T max(T& a, T& b) {
  return a > b ? a : b;
}

int main() {
  int a = 1, b = 2;
  std::cout << "Max (" << a << "," << b << ") ? : " << max(a, b) << std::endl;

  std::string s = "hello", t = "world";
  std::cout << "Max (" << s << "," << t << ") ? : " << max(s, t) << std::endl;
}

이 코드를 실행하게 되면

Max (1 , 2) ? : 2
Max (hello , world) : world

클래스의 템플릿과 마찬가지로 함수가 인스턴스화 되기 전까지 컴파일 시 아무런 코드로 변환되지 않는다. 클래스 인스턴스와 차이가 발생하는 부분은 <>를 통해 타입을 전달해주지 않는다는 것이다. C++ 컴파일러는 똑똑하여, 내가 어떤 자료를 입력하였는지 알아서 인스턴스화 해준다.

🔎 Function Object(Functor)

마치 함수처럼 동작하는 객체를 의미한다. 함수 객체는 특정 조건에 따라 동작하고 이 때, 필요한 데이터를 객체 내에 저장할 수 있다.

아이스크림을 만드는 과정으로 예시를 들어보자

#include <iostream>

class IceCreamMaker {
public:
    // 함수 호출 연산자를 오버로딩
    void operator()(int sugar, int milk) const {
        std::cout << "Mixing " << sugar << "g of sugar with " << milk << "ml of milk." << std::endl;
        std::cout << "Freezing the mixture to make delicious ice cream!" << std::endl;
    }
};

int main() {
    IceCreamMaker iceCreamMaker;

    iceCreamMaker(150, 250);
    iceCreamMaker(120, 180);

    return 0;
}

아이스크림을 만들 때, 우유와 설탕의 양을 계속해서 바꾼다고하면, 함수를 일일이 호출하여 사용하는 것은 불필요한 과정일 것이다. 그래서 클래스 객체를 생성하여, 객체를 호출함으로써 아이스크림을 만드는 방식으로 하면 멤버 변수도 객체 내에 저장이 가능하고 함수 객체를 여러 번 호출하면서 다양한 아이스크림을 만들 수 있다.
최적화의 이점이 있는 것이다.

아래는 조금 더 심화버전의 예시코드이다.

#include <iostream>

template <typename T>
class Vector {
  T* data;
  int capacity;
  int length;

 public:
  // 어떤 타입을 보관하는지
  typedef T value_type;

  // 생성자
  Vector(int n = 1) : data(new T[n]), capacity(n), length(0) {}

  // 맨 뒤에 새로운 원소를 추가한다.
  void push_back(int s) {
    if (capacity <= length) {
      int* temp = new T[capacity * 2];
      for (int i = 0; i < length; i++) {
        temp[i] = data[i];
      }
      delete[] data;
      data = temp;
      capacity *= 2;
    }

    data[length] = s;
    length++;
  }

  // 임의의 위치의 원소에 접근한다.
  T operator[](int i) { return data[i]; }

  // x 번째 위치한 원소를 제거한다.
  void remove(int x) {
    for (int i = x + 1; i < length; i++) {
      data[i - 1] = data[i];
    }
    length--;
  }

  // 현재 벡터의 크기를 구한다.
  int size() { return length; }

  // i 번째 원소와 j 번째 원소 위치를 바꾼다.
  void swap(int i, int j) {
    T temp = data[i];
    data[i] = data[j];
    data[j] = temp;
  }

  ~Vector() {
    if (data) {
      delete[] data;
    }
  }
};

template <typename Cont>
void bubble_sort(Cont& cont) {
  for (int i = 0; i < cont.size(); i++) {
    for (int j = i + 1; j < cont.size(); j++) {
      if (cont[i] > cont[j]) {
        cont.swap(i, j);
      }
    }
  }
}

template <typename Cont, typename Comp>
void bubble_sort(Cont& cont, Comp& comp) {
  for (int i = 0; i < cont.size(); i++) {
    for (int j = i + 1; j < cont.size(); j++) {
      if (!comp(cont[i], cont[j])) {
        cont.swap(i, j);
      }
    }
  }
}

struct Comp1 {
  bool operator()(int a, int b) { return a > b; }
};

struct Comp2 {
  bool operator()(int a, int b) { return a < b; }
};

int main() {
  Vector<int> int_vec;
  int_vec.push_back(3);
  int_vec.push_back(1);
  int_vec.push_back(2);
  int_vec.push_back(8);
  int_vec.push_back(5);
  int_vec.push_back(3);

  std::cout << "정렬 이전 ---- " << std::endl;
  for (int i = 0; i < int_vec.size(); i++) {
    std::cout << int_vec[i] << " ";
  }

  Comp1 comp1;
  bubble_sort(int_vec, comp1);

  std::cout << std::endl << std::endl << "내림차순 정렬 이후 ---- " << std::endl;
  for (int i = 0; i < int_vec.size(); i++) {
    std::cout << int_vec[i] << " ";
  }
  std::cout << std::endl;

  Comp2 comp2;
  bubble_sort(int_vec, comp2);

  std::cout << std::endl << "오름차순 정렬 이후 ---- " << std::endl;
  for (int i = 0; i < int_vec.size(); i++) {
    std::cout << int_vec[i] << " ";
  }
  std::cout << std::endl;
}

우리가 주목해야할 부분을 천천히 살펴보겠다

struct Comp1 {
  bool operator()(int a, int b) { return a > b; }
};

struct Comp2 {
  bool operator()(int a, int b) { return a < b; }
};

struct를 통해, 괄호연산자 operator()를 정의하였고, 선언하는 순간 구조체 객체가 생성이 된다.

Comp1 comp1;
  bubble_sort(int_vec, comp1);

이 객체를 bubble_sort에 전달하게 되면

template <typename Cont, typename Comp>
void bubble_sort(Cont& cont, Comp& comp) {
  for (int i = 0; i < cont.size(); i++) {
    for (int j = i + 1; j < cont.size(); j++) {
      if (!comp(cont[i], cont[j])) {
        cont.swap(i, j);
      }
    }
  }
}

해당함수의 Cont에는 int_vecComp에는 함수객체가 전달되어
!comp(cont[i],cont[j])에서 객체가 함수인 것 마냥 사용되는데 왜 그렇냐면
괄호 연산자인 operator()는 함수를 호출할 때 사용하는 것을 말하는 데 이를 함수 정의시 사용하여 오버로딩하게 되면, 원하는 방식으로 객체를 통해 호출할 수 있도록 만들 수 있는 것이다.
struct에서 operator()(int a, int b)로 오버로딩 하였기 때문에 해당 자료형에 맞는 데이터를 넣게 되면, 작동하게 되는 것이다. 마치 객체를 함수처럼 사용하게 하도록 하는 것이 Functor이다.

❓ 타입이 아닌 template 인자

#include <iostream>
#include <array>

template <typename T>
void print_array(const T& arr) {
  for (int i = 0; i < arr.size(); i++) {
    std::cout << arr[i] << " ";
  }
  std::cout << std::endl;
}

int main() {
  std::array<int, 5> arr = {1, 2, 3, 4, 5};
  std::array<int, 7> arr2 = {1, 2, 3, 4, 5, 6, 7};
  std::array<int, 3> arr3 = {1, 2, 3};

  print_array(arr);
  print_array(arr2);
  print_array(arr3);
}

해당 코드에서는 컴파일 시 T에 어떤 정보를 전달해주고 있을까??

std::array<int, 5> 는 컴파일 시 크기가 5인 정수 배열을 나타내며 타입의 한 종류로 간주가 된다. 템플릿은 컴파일 시간에 타입정보를 바탕으로 코드를 생성하므로, typename T에 배열의 정보가 들어가서, arr.size()를 사용할 수 있는 것이다. 즉 타입 뿐 아니라, 다른 인자도 위와 같은 방식으로 전달이 가능하다는 점을 알 수 있다.

🂡 Default template argument

말 그대로 내가 template argument를 default 값으로 사용할 수 있는 값을 미리 설정해놓는다는 말이다.

아주 간단한 예시코드를 통해 확인해보자.

#include <iostream>
#include <string>


template<typename T = std::string>
T bookstore(std::string a , int b){
  std::cout << "bookname : " << a << std::endl;
  std::cout << "price : " << b << std::endl;
  std::string c = "already had";
  return c;
  
}
  
int main() {
  std::string  a = "harry poter";
  int b = 17000;
  std::cout << "This book is.." << std::endl;
  std::cout << bookstore(a,b) << std::endl;

  return 0;
}

template argumentstd::string으로 설정해놨고, <>를 통해 타입값을 설정하지 않고 함수를 호출하게 되면..

This book is..
bookname : harry poter
price : 17000
already had

이렇게 출력값이 나오게 된다. 아무것도 입력하지 않게 되면 default type인 std::string을 사용하여 함수를 작동하게 된다. 조금 더 응용하는 예시 코드를 보자.

#include <iostream>
#include <string>

template <typename T> struct Compare {
  bool operator()(const T &a, const T &b) const { return a < b; }
};

template <typename T, typename Comp = Compare<T>> T Min(T a, T b) {
  std::cout << std::endl;
  std::cout << "Type : " << typeid(T).name() << std::endl;
  Comp comp;
  if (comp(a, b)) {
    return a;
  }
  return b;
}

int main() {
  int a = 3, b = 5;
  std::cout << "Min " << a << " , " << b << " :: " << Min(a, b) << std::endl;

  std::string s1 = "abc", s2 = "def";
  std::cout << "Min " << s1 << " , " << s2 << " :: " << Min(s1, s2)
            << std::endl;
}

Min함수를 호출하게 될 때, 컴파일 단계에서 자연스럽게 넘어가는 정보인 int형 데이터에 대한 정보를 typename T argument에 받아, 사용하고 있다. 그리고 Min 함수 내에서 typename Comp = Compare<T>로 default argument를 명시하고 있기 때문에, Min 함수 호출 시 Comp type을 명시하지 않게 되면, T에 있는 데이터 값을 그대로 사용한다.

그래서 typename이 궁금하여 직접 출력을 해보았다.

Min 3 , 5 ::
Type : i
3
Min abc , def ::
Type : NSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
abc

이렇게 template T에 데이터값이 잘 들어가고 있음을 확인할 수 있다.

🃏 가변길이 템플릿

파이썬을 사용해본 사람은 알겠지만

print(1 , 3.1 , "abc")
print("asdfasd", var) 

을 실행해보면, 인자로 전달된 모든 것들을 출력할 수 있다.

template를 사용한다면 가능하지 않을까??
그래서 사용하는 기능이 파라미터 팩 이다.

#include <iostream>

template <typename T>
void print(T arg) {
  std::cout << arg << std::endl;
}

template <typename T, typename... Types>
void print(T arg, Types... args) {
  std::cout << arg << ", ";
  print(args...);
}

int main() {
  print(1, 3.1, "abc");
  print(1, 2, 3, 4, 5, 6, 7);
}

typename 뒤에 ...이 오는 것을 template parameter pack이라고 부르고 함수에서 args 앞에 오는 ...function parameter pack이라고 부른다.

두 개다 0개 이상의 argument를 나타내며, 추론된 인자를 제외한 나머지 인자들을 나타낸다.

함수와 템플릿의 차이점은 함수의 경우 ...이 앞으로, 템플릿의 경우 뒤로온다.

위의 코드에서는 재귀적으로 print를 호출하고 있으며, 마지막 인자를 전달 받을 때, 두 개다 호출가능하다. 그러나 C++ 규칙 상 파라미터 팩이 없는 함수가 우선순위가 높기 때문에 첫 번째 함수가 호출되게 된다.

함수의 순서를 뒤집게 되면, C++ 컴파일러는 함수를 컴파일 할 시 자신의 앞에 정의된 함수밖에 보지 못하기 때문에 오류가 발생하게 된다. 그래서 마지막 인자를 호출하게 되면, 결국 print()이 함수내에서 마지막으로 호출되기 때문에, 이 함수를 찾지 못하여 오류가 발생하게 되는 것이다.

템플릿 타입 변수를 인자로 전달하기

#include <iostream>
#include <string>

template<typename T>
class weapon {
private:
    T damage;

public:
    weapon(T dmg) : damage(dmg) {}

    T getDamage() const {
        return damage;
    }
};

template<typename T>
class character {
private:
    std::string name;
    weapon<T> weaponType;

public:
    character(const std::string& charName, const weapon<T>& weapon) : name(charName), weaponType(weapon) {}

    void showCharacterInfo() {
        std::cout << "Character Name: " << name << std::endl;
        std::cout << "Weapon Damage: " << weaponType.getDamage() << std::endl;
    }
};

int main() {
    // 먼저 무기를 설정
    weapon<int> bulldogWeapon(20);

    // 설정한 무기를 가지고 캐릭터를 생성
    character<int> bulldog("Bulldog", bulldogWeapon);

    // 캐릭터 정보 출력
    bulldog.showCharacterInfo();

    return 0;
}

본 내용은 씹어먹는c++을 통해 알게된 내용을 개인적인 공부를 위해 정리한 포스팅입니다. 저작권을 해칠 의도가 없으며, 모든 것은 해당 블로그 저자의 지식재산입니다.
https://modoocode.com/210

0개의 댓글