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 |