Search

CPP Module 07

Created
2021/10/14
tag
42서울
42Seoul
CPP Module
All of C++98
Template
Data Structure
Dynamic Container

Subjects

1. ex00 (A few functions)

Function Template ? Template Function ? Terms !

C++의 꽃은 클래스와 템플릿으로 들 수있는데, Module 07은 템플릿에 대한 내용을 배우게 된다. 특히 ex00함수 템플릿을 정의하고, 템플릿 함수를 호출해보는 것이다. 두 용어의 차이는 잠시 후에 알아보고, 우선 템플릿이라는 개념에 대해 익숙해질 필요가 있다.
사실 Module 07까지 왔다면, 이미 템플릿을 경험해본 바가 있다. static_cast<T>, reinterpret_cast<T>, dynamic_cast<T>, const_cast<T>가 가장 대표적인 예가 된다. 제시된 형 변환들을 이용할 때 임의의 타입을 직접 지정하여 쓸 수 있었고, 임의의 타입 T를 두고 함수를 작성하게 되면 이것이 곧 함수 템플릿이 된다. 즉, 템플릿이라고 하는 것은 제네릭 프로그래밍 (코드를 일반화하는 프로그래밍)에 이용된다.
템플릿은 template이라는 키워드를 명시하는 것으로 정의할 수 있고, 이 때 <>로 감싸진 임의의 타입 T템플릿 매개변수라 한다. 템플릿은 임의의 함수 혹은 클래스를 정의할 때 모두 이용할 수 있으며, template 키워드를 이용하여 정의한 함수를 함수 템플릿이라 하고, 클래스에 대해선 클래스 템플릿이라고 부른다.
그리고 주어진 템플릿을 이용하여 타입을 특정하여 함수를 호출하도록 코드를 작성하게 되면 컴파일 타임에 코드가 생성되는데, 이 때 생성된 함수를 템플릿 함수, 클래스에 대해선 템플릿 클래스라고 부른다. 또한 <> 사이에 특정된 타입을 템플릿 인자라고 부른다.
물론 함수 템플릿 vs 템플릿 함수, 클래스 템플릿 vs 템플릿 클래스, 템플릿 매개변수 vs 템플릿 인자들을 구분 짓는 것이 의미가 없을 때도 있다. 일반적으로 둘 중 어느 용어를 이용하더라도 두 개념을 모두 포괄하는 개념으로 이용할 때가 많기 때문이다. 하지만 코드로 정의된 함수와 코드가 생성된 함수, 코드로 정의된 클래스와 코드가 생성된 클래스, 코드로 정의할 때 사용하는 타입과 코드가 생성될 때 이용한 타입에 대해서 구분을 짓고 싶다면, 이와 같은 구분을 명확히 지을 필요가 있다.

Example

template <typename T> class Something { // ... implementation };
C++
복사
템플릿의 주된 특징은 컴파일 타임과 관련이 있다. 예를 들어 위와 같은 클래스가 있다고 가정해보자. 만일 이 코드가 클래스를 생성하는데 이용되지 않으면, 실제 프로그램 내에는 해당 코드가 포함되지 않는다. 그리고 만일 이 코드가 int 타입의 템플릿 인자로 클래스가 생성되면, int 타입에 대한 코드만 생성되어 프로그램에 포함된다. 그리고 double 타입의 템플릿 인자로 클래스가 생성되면, double 타입에 대한 코드가 생성되어 프로그램에 포함된다. 이와 같은 과정은 모두 컴파일 타임에 이뤄지는데, int 타입 따로, double 타입 따로 코드가 생성된다는 점을 통해 Something<int>Something<double>은 클래스만 같고 전혀 다른 타입임을 알 수 있다.
typenameT가 타입 임을 명확히 명시하는 키워드이다. 명시라는 단어 때문에 typename을 사용하지 않을 수 있냐는 의문이 생길 수 있는데, 템플릿에서는 임의의 타입만 받을 수 있을 뿐만 아니라 특정 값도 받을 수 있다. 이 때 받은 특정 값은 반드시 리터럴 값이어야 하고, 이들은 템플릿의 타입 결정이 이뤄지는 컴파일 타임에 연산식으로 사용될 수 있다. 이와 같이 컴파일 타임에 연산을 끝내는 형태의 프로그래밍을 템플릿 메타 프로그래밍이라고 한다. 즉, 템플릿으로 제네릭 프로그래밍만 할 수 있는 것은 아니다.
런 타임에 값을 할당하는 것은 사용자가 코드를 작성할 때와 동일하기 때문에 금방 이해가 될 것인데, 컴파일 타임에 템플릿으로 값을 할당하는 것이 이해가 안 될 수도 있다. 클래스 템플릿 혹은 함수 템플릿에 특정 값을 컴파일 타임에 결정짓도록 만드는 방법은 static const를 이용하는 것이다. 컴파일 타임에 값을 결정 짓도록 만들기 위해선 그 값을 할당하는 구문을 클래스 템플릿 혹은 함수 템플릿에 명시적으로 작성할 수 있어야 하는데, static const로 정의할 때만 이와 같은 작성이 가능하다. 또한 템플릿으로 생성된 클래스 혹은 함수는 별도의 코드로 생성되어 이용된다는 점을 고려하면, static의 클래스 혹은 함수 내 공유된다는 특징과 const의 불변 특성이 의미 상으로도 매우 적절하다.
template <typename T> void swap(T& a, T& b) { T temp; temp = a; a = b; b = temp; }
C++
복사
템플릿의 동작 원리를 이해했으므로 간단하게 swap 함수에 대해서만 예시를 위와 같이 작성해뒀다. 템플릿을 작성하는 방법이 생각보다 간단하지 않은가? 물론 템플릿을 활용하여 더 어려운 작업을 할 수 있고, 앞으로 하게 될 것이다. 그 전에 기본기를 잘 쌓을 수 있게 많은 연습을 하는 것이 좋다. minmax 함수도 잘 마무리하자.
예시에서 제시된 typename템플릿 매개변수를 정의할 때 사용하는 class 키워드는 용도가 같다. 다만 class 키워드가 먼저 등장했는데, 일반 class를 정의할 때의 키워드와 헷갈리기 때문에 typename이라는 키워드가 추가되었다. 만일 템플릿 매개변수를 정의할 때 class가 나타났다면 typename으로 이해하고, 코드를 읽으면 된다.

2. ex01 (Iter)

Function as a Parameter of Function Template

ex01ex00과 크게 다르지 않은, 함수 템플릿을 정의하는 문제이다. 조금 특이한 점으로는 함수 포인터를 받아와서 이를 호출할 수 있게 만드는 점이다. 아마 Libft에서 ft_lstiter를 기억한다면 이번 함수 포인터의 내용이 쉽게 이해될 것인데, 이번 함수 템플릿for_each를 구현하는 것이다. 다만 ft_listiter 때는 구현해뒀던 t_list에 대해 한정되었다면, 이번에는 임의의 타입으로 정의된 배열에 대해서 각 요소에 함수 포인터의 내용을 적용할 수 있도록 만드는 것이다.
첫 번째 인자는 임의의 타입으로 된 배열로 들어오므로 typenameT에 대한 배열을 T*로 받아오도록 만든다. 두 번째 인자는 크기에 대한 타입이므로 int, unsigned int 등을 직접 명시하여 정의한다. 내 경우에는 std::size_t를 이용했고, 이처럼 고정된 타입을 이용하는 경우에는 typename을 이용하지 않아도 된다. 세 번째 인자는 적용하려는 함수를 참조하는 함수 포인터이다. 이 때 함수의 반환 값은 없어야하고, 함수 호출로 원본 내용이 바뀔 수 있어야하므로 참조자 혹은 포인터를 인자로 받도록 시그니처를 작성하면 된다. 따라서 원본 값의 타입을 알아야 하는데, 이는 배열의 요소와 같은 타입이 되므로 T를 활용하면 된다.

Calling Arugment on Template Function

내 경우에는 iter 함수의 호출 시 이용할 함수를 배열의 각 요소들을 출력해주는 print라는 함수로 두었다. 물론 해당 함수는 사용자들이 어떤 함수로 이용할 것인지에 따라 내용도, 이름도, 정의도 다를 것인데, 이번 문제에 대해선 사용자가 직접 정의하여 이용하려는 함수를 iter 함수에 이용할 때 <>로 타입을 따로 명시하지 않아도 된다는 것을 소개하고자 한다.
int i[5] = { 1, 2, 3, 4, 5 }; iter<int>(i, sizeof(i) / sizeof (*i), print<int>); iter(i, sizeof(i) / sizeof(*i), print<int>); iter(i, sizeof(i) / sizeof(*i), print);
C++
복사
함수 템플릿을 정의할 때 <>그리고 T를 이용했기 때문에 반드시 두 번째 줄과 같은 호출이 되어야할 것 같지만, 사실은 생략이 가능하다. 그 이유는 iter템플릿 인자를 작성하지 않아도 배열에 해당하는 첫 번째 인자를 통해 T를 결정하는 것이 가능하기 때문이다. 따라서 세 번째 줄과 같이 호출하는 것이 가능하다. 더 나아가 print 함수의 템플릿 인자 역시 T를 사용하고 있는데, T는 배열을 통해서 결정이 가능하므로 print템플릿 인자 역시 생략할 수 있다. 따라서 마지막 줄처럼 템플릿 인자를 모두 생략하여 호출하는 것이 가능하다.

3. ex02 (Array)

Definition of Template

ex00 - ex01에서 함수 템플릿을 학습했다면, ex02에서는 Array 클래스를 정의하여 클래스 템플릿을 학습하는 것이 목표이다. 그 전에 하나 짚고 넘어갈 점이 있다. 함수 템플릿클래스 템플릿을 정의할 때 중요한 점이 있다면, 원칙 상 템플릿의 정의는 헤더를 벗어날 수 없다는 것이다. ex00 - ex01에서 무심코 정의를 헤더에 한 사람도 있을 것이고, 서브젝트에 명시된대로 헤더에 정의한 사람도 있을 것이고, 헤더에 선언만 두고 cpp 파일에 정의하려다가 둘러온 사람도 있을 것이다. 경험 해봐서 알겠지만 템플릿으로 정의한 코드들을 cpp 파일로 옮겨서 이용하려고 하면 컴파일이 되지 않는 것을 확인할 수 있다.
템플릿의 특징으로는 컴파일 타임에 모든 연산이 끝난다라는 것이었는데, 이와 같은 특성이 cpp 파일의 컴파일 과정에서 차이가 있기 때문에 정의를 분리할 수 없는 것이다. 조금 더 언급하자면 기본적으로 cpp 파일에 대한 컴파일은 Translation Unit 단위의 파싱에 근거하고 있고, 이 때 포함된 헤더의 선언과 cpp 파일의 정의는 별도의 Translation Unit으로 처리되었다가 종합하게 된다. 하지만 템플릿은 하나의 Translation Unit으로 처리되어야 하기 때문에 cpp 파일처럼 분리하여 정의하게 되면, 컴파일 시에 이를 정확히 인식하지 못하는 것이다. 따라서 ex02에서 Array 클래스 템플릿을 정의할 때는, 제출 파일 목록 때문에도 그렇고 템플릿의 작성 원칙에 따라서도 선언과 정의를 예외적으로 함께 작성해야 한다.
물론 이와 같은 방법이 꽤나 불편하다는 것을 알고 있을 것이다. 헤더에서는 간략하게 선언을 확인하여 구조를 빠르게 파악하고, 실제 정의는 따로 보는 것이 편하기 때문이다. 따라서 ipp 파일이라는 것이 존재한다. 실제로 ipp 파일을 이용하면 템플릿의 선언과 정의를 나눌 수 있다. ipp 파일의 컴파일에 대해선 논리 상으로만 헤더와 나눠놓은 것이기 때문에 헤더의 코드가 Translation Unit으로 변환되기 전, ipp 파일은 헤더로 포함되어 동일한 Translation Unit으로 나올 수 있게 해주도록 만들어져 있다. (당연히 ipp 파일에 대해서 헤더에 포함한다는 명시가 존재해야 한다.)

Destructor & Deep Copy

Array 클래스 템플릿은 이름만 배열이고, STLstd::array와는 그 성격이 매우 다르다. 여기서 구현하는 배열은 std::arraystd::vector 사이 어딘가에 있다는 것을 고려하면 된다. 우선 배열과 비슷하게 이용할 수 있도록 자유 공간에서 동적 할당을 받아 요소를 할당하고 접근할 수 있도록 해야 하므로 임의의 타입으로 된 포인터가 필요하며, 이 때 얼만큼의 크기를 유지하고 있는지 size에 대한 정보도 갖고 있어야 한다.
클래스 내부에 포인터가 들어가면 주의해야 한다고 Module 04에서 언급한 적이 있었는데, 이번에도 Module 04에서 배웠던 내용을 그대로 적용하면 된다. 클래스를 operator=로 할당할 때와 소멸자를 호출할 때는 자신이 기존에 갖고 있던 할당된 메모리를 적절히 해제할 수 있어야 메모리 누수를 방지할 수 있다. 내 경우에는 복사를 위한 반복문을 돌리기 이전에 작성해둔 메모리 할당 구문에서, 할당 받은 공간에서 안전하게 연산할 수 있도록 delete [] → NULL 대입 → size = 0으로 일관되게 처리했다.

operator[] for const or non-const?

operator[]를 정상적으로 작성했다면, constnon-const에 대해서도 operator[]를 이용할 수 있도록 고려가 되었을 것이다. 그렇지 않다면 constnon-const에 대해서 예시를 만들어서 적용해봐도 좋고, 복사 생성자에서 const Array&를 받도록 되어있다면 여기서 값을 복사할 때 문제가 생길 수도 있으니 반드시 확인해야 한다. 동일한 함수 이름에 대해선 Overloading이 가능하기 때문에 2개의 operator[]를 정의할 수 있다. 첫 번째 줄의 코드가 const를 위한 operator[]이고, 두 번째 줄의 코드가 non-const를 위한 operator[]이다. 디버거가 있다면 constnon-const에 대해 어떻게 코드가 흘러가는지 확인해보는 것도 좋을 것이다. 단, const 혹은 non-constoperator[]에 대해 작성된 코드가 같을 수 있어서 다른 타입의 operator[]를 호출하도록 작성했을 수도 있는데, 이와 같은 구문은 무한 재귀에 빠질 수 있으므로 주의해야 한다.

Exception

operator[]를 이용하여 배열의 요소에 접근할 때, index가 맞지 않으면 올바르지 않은 접근이기 때문에 적절히 Exception을 던지고 이를 처리할 수 있어야 한다. Exception에 대해선 사용자가 직접 정의를 하므로 어떤 이름이든 상관 없겠지만, <stdexcept>에서는 이와 같은 경우를 std::out_of_range로 처리하기 때문에 비슷한 이름으로 정의하면 된다.
특히 size, index, n 등에 대해서 타입을 unsigned int로 제시 했기 때문에 이를 이용하면 operator[]의 범위 검사 시에 양수만 비교하면 된다. unsigned이므로 음수가 없기 때문에 int로 처리 했을 때보다 상대적으로 고려할 것이 조금 더 적다. 또한 std::size_tunsigned int와 동일한 별칭이기 때문에 의미 상 더 적절한 std::size_t를 쓴다면 더 좋을 것이다.