2022.11.27 추가 -
VS2022에 std::ranges::views::iota() 가 구현되었다.
이제 다음과 같은 코드를 작성할 수 있다.
auto v = std::ranges::views::iota(0) | std::ranges::views::take(3);
VS2019 에 구현되지 않은 iota_view
를 간단하게나마 구현하면서
커스텀 range 를 만드는 방법을 간략히 알아보자.
참고로, 표준에서 구현될 예정인 iota_view 의 실제 구현 방식은 다음 문서에서 확인할 수 있다.
isocpp.org/files/papers/p0789r0.pdf
만들고자 하는 것 : iota_view
먼저, iota_view
가 어떤 식으로 동작해야 하는지를 알아보자.
<numeric> 헤더에 구현되어 있는 std::iota() 함수는
다음과 같이 특정 숫자부터 1씩 증가시키며 컨테이너를 채우는 용도로 사용된다.
std::vector<int> a;
a.resize(10);
std::iota(a.begin(), a.end(), 0); // 0 1 2 3 4
iota_view
는 위와 같은 행동을 한다.
특정 숫자로부터 1씩 증가하는 range를 만드는 것이다.
즉, 다음과 같이 동작해야 한다.
auto rng = iota_view(0, 5);
std::cout << rng << std::endl; // [0,1,2,3,4]
auto rng2 = iota_view(0) | std::ranges::views::take(3);
std::cout << rng2 << std::endl; // [0,1,2]
자, 이 친구를 어떻게 만들 수 있을까?
range 구현
range
를 구현하려면, 가장 먼저 해야할 것은
std::ranges::view_interface
를 상속받는 것이다.
ranges 의 모태인 ranges-v3 에서는 view_facade 클래스가 있어서,
좀더 쉽게 나만의 view 를 만들 수 있었으나
view_facade 는 표준에 포함되지 않았다.
view_interface
는 CRTP 으로 구현되어 있으므로,
구현할 클래스를 템플릿 파라미터로 전달해야 한다.
class iota_view : public std::ranges::view_interface<iota_view>
{
// To do...
}
이제 필요한 것은 무엇일까?
iota_view
는 range 이므로, std::ranges::range
컨셉을 만족해야 한다.
range
컨셉은 다음과 같다.
// CONCEPT ranges::range
template <class Rng>
concept range = requires(Rng& r) {
std::ranges::begin(r);
std::ranges::end(r);
};
위 컨셉을 만족하려면, begin()
, end()
함수가 있어야 한다.
그리고 begin()
, end()
함수는
iota_view
가 생성될 때 인자로 전달받은
begVale
, endValue
를 가리키는 Iterator
를 리턴해야 한다.
즉, 다음과 같은 형태이다.
class iota_view : public std::ranges::view_interface<iota_view>
{
int begValue = 0;
int endValue = std::numeric_limits<int>::max();
public:
iota_view() = default;
iota_view(int begValue, int endValue = std::numeric_limits<int>::max())
: begValue(begValue), endValue(endValue)
{}
struct Iterator { /* 이것을 구현해야 한다 */ };
Iterator begin()
{
return Iterator(begValue);
}
Iterator end()
{
return Iterator(endValue);
}
};
이제 이 Iterator
만 구현하면,
iota_view
는 완성이다.
iota_view::Iterator
는 당연히, operator++()
가 호출될 때마다
내부적으로 저장하고 있는 숫자가 하나씩 증가하는 형태여야 할 것이다.
이를 위한 가장 기본적인 형태의 Iterator
를 구현하면 다음과 같다.
struct Iterator
{
using value_type = int;
using difference_type = int;
int pos;
Iterator() = default;
Iterator(int pos) : pos(pos) {}
Iterator& operator++() // 레퍼런스를 리턴해야 한다.
{ // weakly_incrementable 컨셉
++pos;
return *this;
}
Iterator operator++(int)
{
int tmp = pos;
++pos;
return Iterator(tmp);
}
// indirectly_readable 및 forward_iterator 컨셉을 만족하려면
// 아래 두 함수의 리턴타입은 같아야 함
const int& operator *() const
{
return pos;
}
const int& operator *()
{
return pos;
}
bool operator==(const Iterator& _other) const
{
return this->pos == _other.pos;
}
};
위 Iterator
는 std::forward_iterator
컨셉을 만족한다.
위 Iterator
를 포함한, iota_view
의 완성본은 다음과 같다.
class iota_view : public std::ranges::view_interface<iota_view>
{
int begValue = 0;
int endValue = std::numeric_limits<int>::max();
public:
iota_view() = default;
iota_view(int begValue, int endValue = std::numeric_limits<int>::max())
: begValue(begValue), endValue(endValue)
{}
struct Iterator
{
using value_type = int;
using difference_type = int;
int pos;
Iterator() = default;
Iterator(int pos) : pos(pos) {}
Iterator& operator++() // 레퍼런스를 리턴해야 한다.
{ // weakly_incrementable 컨셉
++pos;
return *this;
}
Iterator operator++(int)
{
int tmp = pos;
++pos;
return Iterator(tmp);
}
// indirectly_readable 및 forward_iterator 컨셉을 만족하려면
// 아래 두 함수의 리턴타입은 같아야 함
const int& operator *() const
{
return pos;
}
const int& operator *()
{
return pos;
}
bool operator==(const Iterator& _other) const
{
return this->pos == _other.pos;
}
};
Iterator begin()
{
return Iterator(begValue);
}
Iterator end()
{
return Iterator(endValue);
}
};
라이브러리에 올리기엔 허접하지만,
그런대로 예제에서 사용할 수 있을 정도의 커스텀 뷰가 나왔다.
위 iota_view
는 다음의 동작을 정확히 수행한다.
auto rng = iota_view(0, 5);
std::cout << rng << std::endl; // [0,1,2,3,4]
auto rng2 = iota_view(0) | std::ranges::views::take(3);
std::cout << rng2 << std::endl; // [0,1,2]
다행히도, iota_view
에는 operator | ()
를 구현하지 않아도 된다.
이미 구현되어 있는 view
들과 파이프라이닝 될 수 있도록
operator| ()
가 friend 함수로 구현되어 있다.
VS를 사용한다면, ranges
헤더파일에서 "operator|" 를 검색해보는 것이 이해에 도움이 될 것이다.
추가로, 파이프라이닝에는 views::take() 처럼 view 함수객체를 사용하는 것이 편한데,
간략하게 구현해서 사용하려면 다음과 같이 구현하면 된다.
namespace
{
class iota_fn
{
public:
iota_view operator()(
int begValue,
int endValue = std::numeric_limits<int>::max())
{
return iota_view(begValue, endValue);
}
};
inline iota_fn iota; // 함수객체 선언
}
'C++' 카테고리의 다른 글
C++23 : stacktrace (0) | 2022.11.27 |
---|---|
C++20 : std::ranges #3 - range 를 컨테이너로 변환하기 (0) | 2021.03.24 |
C++20 : std::ranges #1 - views (0) | 2021.03.17 |
C++ : shrink_to_fit() (1) | 2020.12.13 |
L1 캐시는 왜 Data와 Inst로 나뉘어 있나요? (번역) (0) | 2020.12.13 |