C++

C++20 : std::ranges #2 - iota_view 만들기

Sorting 2021. 3. 22. 23:43
반응형

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;
	}
};

Iteratorstd::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; // 함수객체 선언
}

 

반응형