Notice
Recent Posts
Recent Comments
Link
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

코딩로그

C++ template과 iterator 본문

코딩로그/C++

C++ template과 iterator

hyeonnny 2019. 12. 6. 16:02

오늘은 template과 iterator에 대해 알아보도록 하겠습니다. 먼저 정의를 해보자면

  • template : 함수나 클래스를 개별적으로 사용하지 않아도, '여러 자료형'으로 사용할 수 있게 만들어 놓은 틀이다.
  • iterator :  container의 (위치값)주소값을 저장하는 point-like object이다. container의 자료형에 구애받지 않는다.

그렇다면 template과 iterator은 대체 왜 배우는 것일까요?

 

바로 "generic programming"을 하기 위해서 입니다.

 

generic이란 specific에 반대되는 개념으로서, 포괄적이고 추상적인 것을 의미합니다.

따라서 programming에서 generic이란 자료형에 구애받지 않는, 클래스나 함수를 specific하게 

특정하지 않고, 한번의 정의로 다양하게 활용할 수 있는 programming이란 걸 알 수 있겠습니다.

 

아직 잘 이해가 가지 않나요? template의 예시를 보며 차근차근 알아보겠습니다.

 

1. template

   1.1 function template

함수 템플릿이란 함수를 만들어낼 때,

함수의 기능은 명확하지만 자료형을 모호하게 두는 것입니다.

1
2
3
4
5
6
7
8
9
# include<iostream>
using namespace std;
 
bool less_than(const int x, const int y) { return x < y; }
 
int main() {
    cout << less_than(23<< endl;
    return 0;
}
cs

위의 예시는 int type인자 두개를 받아 x보다 y가크면 true를, 반대면 false를 return하는 함수입니다.

그런데 만약, double type의 입력인자도 받고 싶다면 어떻게 할까요? 

1
2
3
4
5
6
7
8
9
10
11
# include<iostream>
using namespace std;
 
bool less_than(const int x, const int y) { return x < y; }
bool less_than(const double x, const double y) { return x < y; }
 
int main() {
    cout << less_than(23<< endl;
    cout << less_than(2.45.5<< endl;
    return 0;
}
cs

다음과 같이 입력인자를 double로 받는 함수를 다시 만들어야 할 것입니다.

 

(C++에서는 다형성의 오버로딩 특성에 의해 같은 이름의 함수를 만들 수 있습니다.

이때에는 입력 인자에 따라 알맞는 함수를 불러와서 사용합니다.)

 

그런데 이렇게 여러번 함수를 정의 하지 않아도 되는 방법이 있습니다! 바로 템플릿을 통해서 입니다.

1
2
3
4
5
6
7
8
template<typename T>
bool less_than(const T& x, const T& y) { return x < y; }
 
int main() {
    cout << less_than(23<< endl;
    cout << less_than(2.45.5<< endl;
    return 0;
}
cs

예시코드 인데요, 입력인자의 값이 바뀌지 않기 때문에 const를 써주었고, 메모리 효율을 높이기 위하여 return by reference를 사용하였습니다. template함수를 사용하는 과정을 보면

  • template<typename UserType>을 써준다.
  • 입력 인자 또는 함수의 return type에 해당하는 부분을 UserType으로 바꿔준다.

 

정도로 정리해볼 수 있겠습니다. 이렇듯 템플릿 함수를 사용하는 과정에서 specific funcrions(각각의 데이터 타입을 갖는 함수)들은 C++에 의해 자동으로 generated됩니다.

 

또 두개의 입력인자의 타입이 다를 수도 있습니다. 그럴 때는 

1
2
3
4
5
6
7
8
template<typename T, typename U>
bool less_than(const T& x, const U& y) { return x < y; }
 
int main() {
    cout << less_than(23<< endl;
    cout << less_than(2.45.5<< endl;
    return 0;
}
cs

다음과 같이 typename에 두가지 type을 써주면 됩니다.

 

 

template funcion 예시 - vector를 인자로 받는 template function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>
#include<vector>
using namespace std;
 
int sum(vector<int> v) {
    int result;
    for (int elem : v)
        result += elem;
    return result;
}
 
int main() {
    vector<int> v{ 1,2,3 };
    return 0;
}
cs

dasdf

sum은 벡터를 인자로 받아 simplified for문을 이용해

result에 v의 요소의 합을 저장하고, 이를 return 하는 함수입니다.

이제 이 함수를 template함수로 변형시켜 보겠습니다.

(result = 0으로 초기화 시켜야 합니다. 본 코드에서는 빠져있습니다)

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T>
T sum(vector<T> v) {
    T result;
    for (T elem : v)
        result += elem;
    return result;
}
 
int main() {
    vector<int> v{ 1,2,3 };
    vector<double> v2{ 1.1,2.2,3.3 };
    return 0;
}
cs

 

 

  • 함수 위에 template<typename Type>을 쓴다.
  • int, double등 이전의 type을 모두 Type으로 바꾼다.

 

출력빌드 입니다.

  1.2 class templated

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Point {
private:
    int x;
    int y;
public:
    Point(int _x, int _y);
    void setXY(int _x, int _y);
    int getX() const;
    int getY() const;
    void print() const;
};
 
Point::Point(int _x, int _y) : x(_x), y(_y) {}
void Point::setXY(int _x, int _y) { x = _x, y = _y; }
int Point::getX() const { return x; }
int Point::getY() const { return y; }
void Point::print() const { cout << x << ", " << y << endl; }
 
int main() {
    Point pt1(12);
    pt1.print();
 
    return 0;
}
cs

다음은 int자료형에 대해 작동하는 Point라는 클래스 입니다.

즉, 객체를 생성할 때 입력 인자로 int밖에 받을 수 없습니다.

이제부터 이 class를 template class로 변형시켜보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include<iostream>
#include<vector>
using namespace std;
 
template <class T>
class Point {
private:
    T x;
    T y;
public:
    Point(T _x, T _y);
    void setXY(T _x, T _y);
    T getX() const;
    T getY() const;
    void print() const;
};
 
template<class T>
Point<T>::Point(T _x, T _y) : x(_x), y(_y) {}
template<class T>
void Point<T>::setXY(T _x, T _y) { x = _x, y = _y; }
template<class T>
T Point<T>::getX() const { return x; }
template<class T>
T Point<T>::getY() const { return y; }
template<class T>
void Point<T>::print() const { cout << x << ", " << y << endl; }
 
int main() {
    Point<int> pt1(12);
    Point<double> pt2(1.12.2);
    pt1.print();
    pt2.print();
 
    return 0;
}
cs

자, 변형했습니다! 어느 부분을 바꾼 걸까요?

  • template<class T>를 써준다.(class를 쓰든 typename을 쓰든 전혀 차이없다)
  • int 였던 모든 입력 인자나 return type을 T로 바꾼다.
  • method가 class외부에서 구현될 때 : template<class T>를 함수 윗줄에 삽입, 범위지정 연산자 앞 <T>삽입
  • method를 밖에 빼놓으면 generic function이기 때문에 반드시 template<class T> 하나하나 적어주기!
  • main함수에서 : 객체 정의할 때 class<typename>적어주기.

 

이 과정을 통해 class template을 만들 수 있습니다!

그런데 main함수에서 객체를 정의하는 부분이 무언가와 비슷해 보이지 않나요?

  • Point<int> pt1(1,2);
  • vector<int> v1 {1,2};

 

네, 맞습니다 벡터와 아주 비슷합니다. 여기에서 '벡터또한 템플릿 클래스다'라는 사실을 알 수 있습니다.

 

이렇듯 기본으로 제공되는 템플릿 클래스들이 모여있는 것을 STL(standard template library)라고 하는데요,

이러한 STL은 수많은 generic한 function들과 class들이 모여있는 집합입니다.

 

(또 이런 STL중 standard container도 있습니다.

container란 다른 오브젝트들의 집합을 hold하는 오브젝트 이며,

vector, list, array 등이 있습니다.)

 

또 class를 만들 때는 기본타입으로 작동하도록 만들고 이후 template으로 변형시켜주는 것이 용이합니다!

 

2. iterator

다음은 iterator에 대해 알아보도록 하겠습니다. 

iterator란 컨테이너에 generic하게 작동하는 point-like object입니다.

본격적으로 iterator에 대해 알아보기에 앞서 pointer를 통해 container를 순환했던 과정을 복습해 보도록 하겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
#include<vector>
using namespace std;
 
int main() {
    int ary[] = { 1,2,3 };//왜 []안에 숫자 안들어가냐고? initialize list 제공하니까!
    int* begin = ary;
    int* end = ary + 3;
    for (int* curr = begin; curr != end; curr++)
        cout << *curr << '\t';
    cout << endl;
 
    return 0;
}
cs

다음은 

다음은 위의 코드를 메모리로 정리한 것이다.

(여기부터는 꽤 주관적인 설명이 있을 수 있다.)

메모리 공간은 name,주소, value라는 세가지를 갖는다.

변수를 선언할 때 우리는 name을 지어준다.

name을 지음과 함께 메모리 공간이 할당된다.

 

보통은 변수의 name을 통해 변수를 호출한다. 그런데

예를 들어 호출할 객체가 oxo1~oxo8의 메모리 공간을 차지 한다고

하자. 그러면 전체메모리 공간을 모두 호출해야만 하는 일이 생기고, 너무도 비효율적이다. 따라서

이 객체의 시작 주솟값을 저장하고, 실제로 호출할때는 시작주소값만 가지고 하나하나 읽어간다. 이것이 포인터다.

 

다시 위의 그림으로 돌아가보자. 맨처음 aray를 선언할 때 aray는 oxoo이라는 주소에 oxo1이라는 array의 시작주솟값을 저장한다. 그리고 1,2,3이라는 값은 각각 oxo1,oxo2,oxo3에 저장된다.

 

만약 int a = 1;이라는 값이 있다고 하자. 그렇다면 포인터를 만들때는 int* begin = &a라고 해야할 것이다.(a,oxox,1)

그러나 aray는 value자체에 aray의 시작 주솟값을 저장하므로 int* begin = aray로 &없이 써도 된다.(aray,oxoo,oxo1)

 

또한 curr는 curr++을 할 때마다 다음 aray요소의 주솟값으로 넘어간다. 그러다 end, 즉 요소 끝의 쓰레기공간을 만나면 for문을 정지한다.

 

2.1 iterator

자, 여기까지 이해했다면 iterator을 어렵지 않게 이해할 수 있을 것이다. 시작해보자.

iterator은 객체이다. 따라서 관련 method와 연산자가 존재한다.

  • 주요 method : v.begin() = begin(v), v.end() = end(v)
  • 주요 연산자 : iter++, iter--, *iter, iter1 != iter2

 

1
2
3
4
5
6
7
8
9
int main() {
    vector<int> v{ 1,2,3 };
    auto iter_begin = begin(v);
    auto iter_end = end(v);
    for (auto iter = iter_begin; iter != iter_end; iter++)
        cout << *iter << '\t';
    cout << endl;
    return 0;
}
cs

위의 예제와 거의 비슷한 모습을 보입니다. 다른 점을 꼽자면

위에서는 end = aray+3을 해서 size를 알았던 반면에,

여기서는 end(v)함수가 있어 자동으로 마지막 값 다음 값의 주솟값을 할당해 준다는 것 입니다.

 

또 pointer와 iterator이 무엇이 다른지 궁금해 비교분석을 해보았습니다.

 

  • pointer와 iterator 비교분석
  1. iterator과 pointer모두 주솟값을 저장한다.
  2. pointer는 datatype이 specific하다.ex) int* begin;
  3. 그러나 iterator은 datatype이 generic하다. ex) auto iter_begin