Search

CPP Module 08

Created
2021/10/15
tag
42서울
42Seoul
CPP Module
All of C++98
STL
Containers
Iterators
Algorithm
Template
Data Structure
Exception

Subjects

1. ex00 (Easy find)

Day Specific Rule

이번 Module 08에서는 Module 07의 템플릿을 이용하지만, 예외적으로 이전 서브젝트들과 달리 STL을 허용한다. STL에서 소개되는 Container, Iterator, Algorithm 모든 것을 허용한다. 물론 Module 08에서 STL을 최대한 지양하면서 코드를 완성해도 되겠지만, 서브젝트에서는 STL의 사용을 적극 권장하고 있고, 이를 사용하지 않는 행위를 게으르다고 표현했으니 STL을 최대한 활용하여 작성하도록 노력하는 것이 좋다. 참고로 C++11을 사용하라는 것이 아니니 아래에 설명된 std::begin, Parameter Pack, Lambda 등을 사용하지 않도록 주의하자.
ft_containers를 구현하는데 많은 도움이 된다.

STL?

위에서 말한 것처럼 STLContainer, Iterator, Algorithm을 모두 포함한 개념으로 Standard Template Library의 약자이다. 이름에서 알 수 있듯이 템플릿을 적용한 라이브러리이므로, 임의의 타입에 대해서 Container, Iterator, Algorithm을 모두 지원한다.
대체적으로 많은 사람들이 STL하면 Container를 떠올리곤 하는데, 이는 STL의 나머지 요소들이 Container를 기반으로 동작하기 때문이라고 볼수 있다. STL의 꽃인 Container는 임의의 타입에 대해 동작하는 자료구조를 구현한 것들을 총칭한다. 또한 Container 내부에는 Container의 요소들을 순회하고, 탐색하고, 이용할 수 있도록 Iterator를 지원하는데, 이는 Module 00에서 언급했던 것처럼 포인터처럼 동작하는 객체를 의미한다. IteratorContainer를 통합된 방식으로 관리하고, Algorithm에 대해서 통합된 코드로 구현하기 위해 생긴 개념이다. 마지막으로 AlgorithmContainer를 대상으로 적용할 수 있는 일반적인 기능들 모두를 의미하고, <algorithm>을 포함하면서 각 함수들을 호출할 수 있다. 예를 들어 std::swap, std::min, std::max, std::find, std::search, std::sort 등이 존재하고, 이 외에도 Container를 대상으로 하는 정말 많은 함수들이 있다. 해당 함수들은 주로 ContainerIterator를 조작하면서 동작하고, 템플릿 특수화 (Template Specialization)를 통해 배열에 대해서도 포인터로 동작할 수 있도록 되어 있다.
STL에 대한 간략한 소개는 Module 00ex02에도 있으니 이를 읽어보자. 그리고 템플릿 특수화에 대해선 그 아래의 글을 읽어보자.

std::find in STL

easyfind라고 하는 함수는 Container에서 특정 값을 찾는 함수이다. 특정 값을 찾지 못하면 Exception을 던지거나, 그에 걸맞는 값을 반환하도록 구현해야 한다. 이 특성을 고려하여, 특정 값을 찾았을 때 역시 어떤 값을 반환하도록 구현하면 된다.
내 경우에 easyfind의 의미는 std::find 함수를 이용하여 그 값을 찾아내고, 없다면 Exception을 던져주는 함수로 이해했다. 물론 easyfind의 결과를 일반적인 Algorithm의 함수들처럼 Container의 끝을 반환할 수 있도록 std::end를 적용한 Iterator를 반환해도 되겠지만, 이렇게 하면 정확하게 std::find와 역할이 겹치기 때문에 Exception을 던지도록 구현했다. 또한 이와 같은 기능이 std::find와 겹치지 않는다고 생각해서 std::find를 호출하여, 해당 함수를 한 번 더 Wrapping한 함수로 동작할 수 있도록 만들었다.
Exception은 직접 구현해도 좋겠지만, <stdexcept>에 있는 std::runtime_error를 이용했고, 해당 Exception생성자 인자로 별도의 리터럴 값을 넘겨서 what 멤버 함수로 Exception의 내용을 확인할 수 있도록 만들었다.

Parameter Pack (사용하면 안 됩니다! 알아만 두세요.)

int main(void) { std::deque<int> d; std::list<int> l; std::vector<int> v; test(d, 3, 1, 2, 3, 4, 5, 6, 7, 8, 9); test(l, 6, 1, 2, 3, 4, 5, 6, 7, 8, 9); test(v, 10, 1, 2, 3, 4, 5, 6, 7, 8, 9); return (0); }
C++
복사
구현한 easyfind라는 함수가 int 타입을 저장할 수 있는 임의의 Container에 대해서 동작한다는 것을 증명하기 위해 일일이 Container를 만들고, 값을 넣고, easyfind를 호출하고 ... 너무 번거롭지 않은가? 이를 극복하기 위해서, 기존에 생성해둔 Container를 함수의 인자로 받아 유동적으로 내부 요소 값을 할당하여 easyfind를 호출해주는 함수를 만들었다. 예를 들어 함수 이름이 test라고 하면, main 함수의 내용은 위 코드가 전부이다.
template <typename C, typename... Arg> void test(C& container, int value, Arg... args) { container = C{args...}; try { typename C::iterator iter = easyfind(container, value); std::cout << "Value " << value << " found on Index " << std::distance(std::begin(container), iter) << std::endl; } catch (std::exception& e) { std::cerr << e.what() << std::endl; } }
C++
복사
코드를 보면 대충 예상할 수 있을 것인데, 첫 번째 인자가 Container, 두 번째 인자가 찾으려는 값, 세 번째 인자가 Container가 보유하려는 요소들이 된다. Container의 사이즈를 미리 정해둔 것도 아니고, Container의 사이즈를 함수의 인자로 넘긴 것도 아닌데, 위와 같은 구문이 어떻게 가능한지 의문이 들 수 있다. 이와 같은 구문이 가능한 것은 템플릿에서 Parameter Pack을 이용했기 때문이다. Parameter Pack...로 작성한 부분을 말하며, 그 생김새가 Variadic Argument (가변 인자)와 매우 유사하다.
Parameter PackVariadic Arugment와 비슷하게 생겼다고 해서 서로 동일 한 것은 아니다. Variadic Arugment의 경우 인자를 명시하지 않으면 올바르지 않은 구문이라고 보기 때문에 컴파일러 경고를 내주지만, Parameter Pack은 기본적으로 0개 이상의 인자를 지칭할 때 사용된다.
템플릿 매개변수에서 이용한 Parameter Pack템플릿 인자로 사용하기 위해 정의된 Parameter Pack의 생김새 때문에 코드를 작성할 때 매우 헷갈릴 수 있는데, 펼친다 → ... → 넣는다 라고 생각하면 편하다. typename... Arg여러 인자들을 펼친다 → ... → 여러 인자들을 Arg라고 생각한다로 이해하면 되고, Arg... args여러 인자들을 의미하는 Arg를 펼친다 → ... → 펼친 여러 인자들을 args라고 생각한다로 이해하면 된다. Arg는 타입이고, args는 변수이므로 정확하게 위에서 언급한 것처럼 동작하는 것은 아니지만, 이해하는데는 크게 문제가 없다. 또한 args라는 변수를 이용할 때는 끝에 ...를 붙여 펼쳐서 여러 인자로써 이용하도록 만들어야 한다.
container = C{args...}C가 특정 타입의 Container를 의미하므로, 해당 타입의 생성자Uniform Initialization으로 호출하여 container에 할당하는 것을 의미한다. Uniform Initialization은 중괄호를 이용하여 초기화하는 기법을 의미하며, 생성자를 호출했을 때는 std::initializer_list<T>를 인자로 이용하는 구문이 실행된다. 그리고 해당 생성자는 중괄호에 명시된 값들을 내부 요소로 이용할 수 있게 만든다. 따라서 중괄호 내에 명시한 값들은 모두 타입이 같아야 std::initializer_list<T>로 인자를 넘길 수 있다.

std::begin ? or Member Function begin ? (사용하면 안 됩니다! 알아만 두세요.)

이제까지 예시를 보면 std::begin을 주로 이용했는데, Container를 대상으로 한다면 std::begin을 쓰든 멤버 함수 begin을 사용하든 개인의 선택에 달려있다. 하지만 <algorithm>에서 제공하는 std::begin의 용도는 Container 뿐만 아니라 배열에 대해서도 적용될 수 있도록 해준다. 즉, std::begin 함수가 멤버 함수보다 조금 더 보편적인 경우까지 고려할 수 있으므로, Container만 대상으로 하지 않는 코드를 작성해야 된다면 std::begin을 사용하는 것이 좋다.
위에서 Template Specialization을 찾아봤다면, std::begin이 포인터도 처리할 수 있는 것이 이 덕분임을 눈치챌 수 있을 것이다.

Dependent Type

template <typename C, typename... Arg> void test(C& container, int value, Arg... args) { container = C{args...}; try { typename C::iterator iter = easyfind(container, value); std::cout << "Value " << value << " found on Index " << std::distance(std::begin(container), iter) << std::endl; } catch (std::exception& e) { std::cerr << e.what() << std::endl; } }
C++
복사
마지막으로, 모든 Container들은 Iterator를 갖고 있으며 내부에 별칭으로 iterator 등을 갖고 있기 때문에 Container를 지칭하는 C에 대해 C::iterator를 이용할 수 있다. CContainer로 들어오지 않으면 iterator가 없을 확률이 높으며, 일반 타입에 대해선 내부에 iterator 별칭을 갖고 있지 않기 때문에 해당 구문으로 잘못 기재된 인자에 대해서 컴파일러가 걸러낼 수 있게 해준다.
혹여나 직접 정의한 클래스에 iterator 별칭이 존재한다면 test 함수가 그대로 이용되는 것 아닌가 하고 걱정을 할 수도 있는데, 정의된 클래스가 iterator를 갖고 있다는 전제가 Container로써 구현된 것을 의미하므로 test 함수가 이용될 수 있어야 한다. 클래스를 정의할 때 아무런 의미 없이 iterator를 정의했다면, 이에 대해선 다시 고려해볼 필요가 있다.
이 때 조금 특이한 구문이 있다면, C::iterator 앞에 typename이 존재한다는 것이다. 템플릿을 정의하기 위해 template 구문에서 typename을 이용한 것도 아닌데, 왜 덩그러니 C::iterator 앞에 typename이 존재하는지 의아할 수 있다. Module 07의 글을 유심히 읽었다면, 템플릿 매개변수는 타입으로 받도록 정의할 수도 있고, 값으로 받도록 정의할 수 있다는 것을 알고 있을 것이다. 즉, C가 값이든 타입이든 :: 라는 Scope 연산자를 이용할 수 있다면, 내부 요소에 접근할 수 있다.
C::를 사용할 수 없다면, 컴파일러는 사전에 이를 감지하고 해당 함수를 호출할 수 없다고 알릴 것이다.
C가 갖고 있는 요소들을 자유롭게 이용할 수 있기 때문에 프로그래머에게 주어지는 자유도는 높아지지만, 불행하게도 컴파일러는 ::로 접근한 내부 요소가 타입인지 값인지 판별하지 못한다. 따라서 ::로 내부 요소에 접근한 경우 반드시 타입인지 값인지 구분해줘야 하는데, 타입인 경우에는 typename을 명시적으로 기재해야 한다. 이와 같이 템플릿 인자::로 이용했을 때 typename을 명시한 타입들을 Dependent Type (의존 타입)이라고 한다.
만일 위의 C::iterator에서 typename을 제거하고 컴파일 해보면, 설명한대로 컴파일러는 C::iterator가 값인지 타입인지 알 수 없기 때문에 Ambiguity로 에러를 내는 것을 볼 수 있다.

2. ex01 (Span)

Algorithm

ex01Module 08에서 가장 STL이 필요한 문제가 아닌가 라고 생각한다. Span 클래스생성자의 인자로 받은 값만큼만 내부 요소를 둘 수 있고, 내부 요소의 추가를 addNumber를 통해 추가할 수 있다. 특히 shortestSpanlongestSpan을 찾아내려면 <algorithm>의 함수를 이용하는 것이 편하기 때문에, 직접 동적 할당을 받아서 값을 저장하는 식으로의 운용보다는 Container를 이용하여 요소 저장과 로직에서 모두 득을 보는 것이 좋다고 생각한다. 적절한 Container를 찾게 되면, DestructorDeep Copy를 신경 쓸 필요 없을 뿐만 아니라 (각 Container 클래스의 멤버 함수로 정의되어 있으므로), 요소 유지와 addNumber의 구현도 상당히 간단해진다.
shortestSpan은 내부 요소의 최소 값그 다음 최소 값과의 차이를 의미하고, longestSpan은 내부 요소의 최소 값최대 값의 차이를 의미한다.
내 경우에는 std::vector<int>를 이용했고, 추가적인 index를 정의하여 이용하는 것보다는 std::vector<T>size 멤버 함수와 capacity 멤버 함수를 이용하는 식으로 코드를 작성했다. 특히 std::vector<int>private 영역으로 두면서, std::vector<int>에 접근할 수 있도록 getData 멤버 함수를 정의해뒀다.
Span 클래스의 내부 요소를 수정하지는 않는다는 것을 가정하여 getDataconst std::vector<int>&를 반환하는 const 멤버 함수로 정의해뒀다. 만일 수정을 가능하게 만들고 싶다면 const가 모두 빠진 getData 멤버 함수를 하나 더 Overloading 하면 된다.
// s -> Span Object // _data -> std::vector<int> Object _data.reserve(s.getData().capacity()); std::copy(std::begin(s.getData()), std::end(s.getData()), std::back_inserter(_data));
C++
복사
총 가용 요소의 수현재 요소의 수를 별도의 변수로 두지 않고 운용했다고 했기 때문에, 생성자operator=에서는 capacity를 이용하여 총 가용 요소의 수로, size를 이용하여 현재 요소의 수로 이용되도록 구현했다. 이에 따라 std::vector<int>의 생성자로 미리 size를 할당 받아 이용하는 방식 보다, reserve 멤버 함수로 capacity만 늘리고 std::copy를 이용할 때는 std::back_inserter를 이용하여 내부적으로 push_back 멤버 함수를 호출하여 size를 늘리는 방식으로 구현했다.
기본 생성자sizecapacity0일 수 있도록 std::vector<int>에 대해서 reserve(0)로 호출하도록 만들었고, 애초에 기본 생성자를 호출하지 못하도록 private 영역으로 빼두었다. 또한 std::size_tcapacity를 제공 받는 생성자에서는 std::vector<int>의 생성자로 size를 늘려서 미리 할당을 받도록 만드는 것이 아니라, 단순히 reserve 멤버 함수만 호출하여 capacity만 늘려두었다.

Exception

Span 클래스에서 정의해야 하는 Exceptionstd::vector<int>capacity가 꽉차서 요소 추가가 안 되는 CannotStoreException과 내부 요소가 1개만 존재해서 Span을 할 수 없는 경우의 CannotSpanException으로 2개가 필요하다.
addNumber를 이용할 때 CannotStoreException을 이용하게 되는데, std::vector<T>는 내부에 할당된 capacity가 부족하더라도 push_back 멤버 함수로 size를 늘려서 capacity보다 size가 커지게 되면 자동으로 capacity를 늘리도록 구현되어 있기 때문에 push_back 멤버 함수의 호출 전에는 capacity 멤버 함수와 size 멤버 함수의 호출로 두 값을 비교하여 적절하게 Exception을 던지는 것이 중요하다.
shortestSpanlongestSpan은 내부 요소가 1개인 경우에는 Span을 찾을 수 없으므로 로직을 수행하기 전에 반드시 size 멤버 함수를 호출하여 요소의 수를 검증하는 과정이 필요하다.

longestSpan

longestSpan의 경우 내부 요소의 최소 값과 최대 값만 찾으면 되므로, std::minstd::max를 한 번씩 호출해서 풀면 된다. 이 때 std::minmax_element라는 함수를 이용하면 pair<ForwardIterator, ForwardIterator> 형태로 최소 값의 위치와 최대 값의 위치를 받을 수 있으므로, pair. firstpair.second를 각각 참조하여 뺀 값을 반환하도록 구성해도 된다. (pairfirstsecondIterator이므로 역참조하여 값을 얻어내야 한다.)
std::minmax라는 함수도 있는데 해당 함수의 원형을 보면, std::initializer_list<T> 혹은 2개의 인자만 들어올 수 있으므로 여기서는 적절하지 않다.

shortestSpan

가장 작은 2개 값을 찾는 잘못된 풀이 (jeongwle님 감사합니다)
자칫 위의 토글에서 제시한 것처럼 가장 작은 2개 값을 찾는 것으로 오해할 수 있으니 주의하자. 그저 구간 내에 존재하는 차이가 가장 작은 값을 찾아내면 된다. 이번 문제를 해결하기 위해서 std::adjacent_difference를 활용할 것이다.
std::vector<int> diff(_data); std::adjacent_difference(std::begin(diff), std::end(diff), std::begin(diff)); std::transform(std::begin(diff), std::end(diff), std::begin(diff), [] (int& i) { return std::abs(i); }); return ((*std::min_element(std::begin(diff) + 1, std::end(diff))));
C++
복사
위 코드와 같이 기존 요소들을 유지할 수 있도록 복사 생성자std::vector<int>를 생성한 후, 인접한 요소들 간의 차이를 구하는 std::adjacent_difference를 호출한다. 해당 함수는 <numeric>에 존재하며, 첫 번째와 두 번째 구간 사이의 각 인접 요소 간 차이를 구할 때 세 번째 인자에서부터 기록하게 된다. 차이 자체는 양수일 수도 있고 음수일 수도 있기 때문에, std::transform을 호출하여 차이 값을 유지하고 있는 diff의 요소를 절대 값으로 바꿔주면 된다. 마지막으로 가장 작은 값을 찾아야하므로 std::min_element로 위치를 찾은 후, 역참조한 값을 반환하면 된다.
참고로 편의성 때문에 위의 CallableLambda로 작성되어 있지만, 서브젝트에서는 C++98을 지켜야하므로 이를 사용해선 안 된다. Lambda의 모체인 Functor를 찾아서 구현해보자.

addRange

문제를 잘 읽어보면 요구 사항은 아니지만, Iterator를 활용하여 Range로 내부 요소를 추가할 수 있도록 두면 더 좋을 것 같다고 했다. 이를 구현하는 방법에 대해서도 알아보자.
기본적으로 Range-BasedContainerIterator와 배열의 포인터에 대해서도 동작할 수 있어야 한다. 즉, Span 클래스addRange라는 함수는 임의의 Container에 대해서도 동작할 수 있어야 하므로, 함수 템플릿으로 정의되어야 한다. 이 때 Iterator도 받을 수 있어야 하고, 포인터도 받을 수 있어야 하므로 어떻게 typename을 정의하면 좋을지 고민이 될 것인데, 내부에서 순수하게 로직을 직접 구현하면 이것이 문제가 될 수 있지만 STL의 함수를 호출하는 식으로 Wrapping 하면 크게 문제 될 것이 없다.
std::vector<T>std::deque<T>Random Access Iterator를 이용하도록 구현되어 있기 때문에, 포인터와 동일한 성질을 갖는다. 따라서 순수하게 로직을 직접 구현해도 문제가 되지 않는다. 하지만 std::list<T>Bidirectional Iterator를 이용하도록 구현되어 있는데, 이는 포인터의 사칙 연산을 제공하지 않으므로 STL을 이용하지 않고 직접 구현하면 문제가 될 수 있다.
if (std::distance(begin, end) > static_cast<long>(_data.capacity() - _data.size())) throw (CannotStoreException());
C++
복사
Range-Based로 내부 요소를 추가하려는 경우, 현재 capacitysize의 차이가 각 Iterator의 차이보다 작다면 함수를 수행할 수 없으므로 Exception을 던져야 한다. 여기서 차이를 구하는 부분이 문제가 될 수 있는 부분이고, 특정 ContainerIterator는 차이를 구하는 operator-가 지원되지 않는 것이 문제의 원인이다. 이를 극복하기 위해선 std::distance를 이용하면 되는데, std::distance<algorithm>에서 제공되기 때문에 포인터에 대해서도 동작하고 Iterator에 대해서도 동작하며, 두 인자의 차이 값을 반환해준다.
두 차이 값의 비교가 끝나고 내부 요소를 추가하는 것이 가능한 상황이라면, Container들의 Iterator++, --, *을 지원하므로 std::vector<int>push_back 멤버 함수를 호출하여 위 코드처럼 내부 요소를 추가하도록 작성했다.
std::stack<T>, std::queue<T>, std::priority_queue<T>Adaptor ContainerIterator를 지원하지 않으므로 addRange 후보로 적절하지 않아 인자로 들어오지 않는 것을 전제로 뒀다.

3. ex02 (Mutated abomination)

Adaptor Container

ex01의 마지막 부분에 std::stack<T>, std::queue<T>, std::priority_queue<T>를 언급하면서 Adaptor Container를 볼 수 있었다. 우선 Container들의 분류는 특성에 따라 아래와 같이 나뉜다.
Sequential Container
std::vector<T>, std::deque<T>, std::list<T>, std::forward_list<T>
Adaptor Container
std::stack<T>, std::queue<T>, std::priority_queue<T>
Associative Container
std::map<T>, std::multimap<T>, std::set<T>, std::multiset<T>
Unordered Associative Container
std::unordered_map<T>, std::unordered_multimap<T>, std::unordered_set<T>, std::unordered_multiset<T>
잘 생각해보면 Adaptor Container에 명시된 3개의 Container는 선형적인 특성을 갖고 있는 것을 떠올릴 수 있는데, 왜 Sequential Container가 아닌지 의아할 것이다. 이들은 선형적인 특성을 갖고는 있지만 특정 목적에 활용할 수 있도록 정의되었고, 이에 따라 선형적인 특성을 갖고 있는 Sequential Container를 기반으로 생성된다. 이들의 공통적인 특성으로는 Iterator를 갖고 있지 않아, 순회, 탐색, 등이 불가능하고, 각 Container의 특성에 맞게 한 번에 한 요소만 접근할 수 있다는 것이다.
기반으로 생성된다는 것이 곧 상속을 의미하는 것이 아니다. 선택된 Sequential Container를 적절히 캡슐화하여 내부 로직을 가공해서 Adaptor Container로 제공된다.
std::stack<T>은 템플릿 인자로 타입을 명시하면, 선택적으로 두 번째 인자로 어떤 Sequential Container를 이용하여 std::stack<T>을 생성할 것인지 정할 수 있다. std::stack<T>에 이용할 수 있는 Sequential Containerstd::vector<T>, std::list<T>, std::deque<T>이며, 별도로 명시하지 않으면 템플릿 기본 인자에 따라 std::deque<T>를 이용한다.
이와 같은 특성은 std::queue<T>std::priority_queue<T>도 동일하다. std::queuestd::list<T>std::deque<T>를 이용하여 생성될 수 있으며, 기본 설정은 std::deque<T>이다. std::priority_queue<T>std::vector<T>std::deque<T>를 이용하여 생성될 수 있으며, 기본 설정은 std::deque<T>이다.
이제 ex02가 요구하는 바가 무엇인지 감이 올 것이다. std::stack<T>Adaptor Container이므로 Iterator를 지원하지 않기 때문에, Iterator를 적절히 추가하여 순회 및 탐색이 가능하다록 만드는 것이다.
일반적으로 이처럼 순회 및 탐색이 필요한 경우에는 std::stack<T>를 이용하지 않는다. 만일 순회 및 탐색이 필요한 경우라면 다른 자료구조를 선택하는 것이 더 좋은 선택이므로, 용도에 맞는 자료구조를 고민하는 습관은 굉장히 중요하다. 또한 분명 Adaptor Container가 없어도 Sequential Container 만으로 동일한 기능을 할 수 있음에도 Adaptor Container가 존재하는 것은, 의미 상 더 적절한 형태의 자료구조를 이용하기 위해서이다.

Kinds of Iterators & Alias

C++11 이전에도 Container라는 개념은 존재했었는데, C++11이 오면서 여러 Container들을 묶어 STL로 취급되었고 이에 따라 기존 Container에서 제공하는 멤버 함수보다 더 많은 기능들이 추가 되었다. 특히 이번 문제처럼 Iterator를 이용해야 하는 경우 C++98에서는 beginend 멤버 함수만 존재 했다면, C++11에서는 cbegincend, rbeginrend, crbegincrend가 추가 되었다. C++11의 편리한 기능들을 Module 08에서 이용할 수 있도록 허용해줬으므로 이들을 구현해줄 필요가 있지 않은가?
위에서 분류된 Container들은 내부에 여러 Iterator가 (조금은 복잡한 구조로) 정의되어 있고, 그 종류는 iterator, const_iterator, reverse_iterator, const_reverse_iterator로 나뉜다. iterator를 이용하는 함수가 beginend, const_iterator를 이용하는 함수가 cbegincend, reverse_iterator를 이용하는 함수가 rbeginrend, const_reverse_iterator를 이용하는 함수가 crbegin, crend가 된다.
기본적으로 Iterator는 상속 구조에 따라 InputIterator, OutputIterator, ForwardIterator, BidirectionalIterator, RandomAccessIterator로 나뉘고, 각각의 카테고리 태그를 갖고 있다. 그리고 Sequential ContainerAdaptor Container의 관계처럼 Iterator 역시 Adaptor Iterator가 존재한다. 제시된 5개의 Iterator들을 적절히 가공하여 역방향으로 접근하는 reverse_iterator, 요소 수정을 막는 const_iterator, 그리고 두 속성을 모두 가진 const_reverse_iterator를 만들어 낼 수 있다.
Iteratorconst 속성 적용은 시작 지점과 끝 짐의 영향 없이 해당 지점의 요소를 수정할 수 있는지 없는지만 결정된다. 하지만 Iteratorreserve 속성은 적용 여부에 따라 시작 지점과 끝 지점을 의미하는 beginend의 참조 지점이 달라진다. 따라서 Iterator에 대해 정확히 이해하는 것이 중요한데, 이에 대해선 아래 링크의 제 39장 반복자 부분으로 꼼꼼히 학습하자. 제일 중요한 것은 end가 참조하는 지점이 내부 요소의 끝 지점이 아니라 끝에서 1칸 더 이동한 지점임을 명심하자.
상속한 std::stack<T>Iterator를 지원하지 않아서 어떻게 해야할지 막막하겠지만, 각 Iterator에 해당하는 함수의 반환 값을 만들어 내는 방법은 간단하다. std::stack<T>Adaptor Container라는 점을 명심하면, std::stack<T>를 만들 때 사용된 Container 타입에 접근할 수 있다는 것을 짐작할 수 있다. 내 경우엔 기본 설정을 이용하는 std::stack<T>를 상속받도록 뒀기 때문에 std::deque<T> 타입을 활용할 수 있다. std::stack<T>의 정의를 확인해보면 container_type이라는 별칭이 존재하는데, 이 타입 정의를 이용하면 std::deque<T>를 타입으로 이용할 수 있다. 또한 std::stack<T>container_type으로 된 객체는 c로 접근할 수 있다는 것도 확인할 수 있다. 해당 객체는 protected 영역에 존재하기 때문에 지금처럼 파생 클래스로 만든 MutantStack에서도 std::deque<T> 객체를 접근할 수 있다.
iterator begin(void) { return (this->c.begin()); } iterator end(void) { return (this->c.end()); } const_iterator cbegin(void) const { return (this->c.cbegin()); } const_iterator cend(void) const { return (this->c.cend()); } reverse_iterator rbegin(void) { return (this->c.rbegin()); } reverse_iterator rend(void) { return (this->c.rend()); } const_reverse_iterator crbegin(void) const { return (this->c.crbegin()); } const_reverse_iterator crend(void) const { return (this->c.crend()); }
C++
복사
따라서 각 iterator를 이용한 beginend는 위와 같이 간단히 구현할 수 있다. 하지만 위처럼 단순히 iterator, const_iterator, reverse_iterator, const_reverse_iterator를 이용하려고 하면, MutantStack<T>에서는 해당 타입들이 무엇인지 모르기 때문에 별도의 별칭 정의가 필요하다.
별칭 정의를 할 때는 typedefusing을 이용할 수 있는데, typedefusing과 동일한 기능을 하는 것은 아니지만 적어도 별칭 정의에 대해선 typedefusing은 동일한 방법으로 정의되어 있다. 하지만 이왕 이용할 것이라면 using을 좀 더 애용하는 습관을 들일 수 있도록하자. 이에 대해선 아래 링크에서 차이점과 그 이유에 대해서 잘 설명되어 있으니 이를 참고하면 된다.
내부적으로 별칭 정의에 대해선 typedefusing은 로직이 동일하게 구현되어 있다.
std::stack<T>의 정의를 확인하면서 여러 작업들을 할 수 있었듯이, 별칭 정의는 직접 찾아서 해보도록 하자. 단, 주의할 점이 있다면 위에서 배웠던 의존 타입에 대해서 typename을 반드시 기록할 수 있도록 하자.