C++

C++20 : std::ranges #3 - range 를 컨테이너로 변환하기

Sorting 2021. 3. 24. 00:39
반응형

요약


지금은 방법이 없다.

ranges::common_view 가 VS에 구현될 때까지 다음 함수를 정의해서 쓰자.

template<class Rng>
requires std::ranges::range<Rng>
auto toVector(Rng& rng)
{
	std::vector<std::ranges::range_value_t<decltype(rng)>> v;

	if constexpr (std::ranges::sized_range<decltype(rng)>) {
		v.reserve(std::ranges::size(rng));
	}

	std::ranges::copy(rng, std::back_inserter(v));
	return v;
}

 

 

서론


2021년 3월, VS2019 를 기준으로 포스팅을 하다보니, 아쉬운 점이 많다.

컨테이너를 range 로 변환할 때에는 다음과 같이 views::all() 을 사용하면 되니 간단하다.

  std::vector<int> v = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
  auto r = std::ranges::views::all(v);

그런데 반대로, range를 컨테이너로 변환하려면 물음표가 뜬다.

어떤 방법이 있을까?

 

Try 1 : for 문


생각나는 가장 간단한 방법은 아마, 다음과 같이  for문을 사용하는 방법일 것이다.

std::vector<int> v;
for (int i : rng)
{
	v.push_back(i);
}

자고로 눈물나는 방법이 아닐 수 없다.

 

ranges 라는 선진문물을 들여놓고 이렇게 사용하기엔 뭔가 아쉽다.

설탕이 조금 필요해 보인다.

 

Try 2 : ranges::to() 


사실, ranges 의 모태인 range-v3 에서는 다음과 같이 컨버팅 함수를 제공했었다.

auto v = std::ranges::to<std::vector>(r);

아쉽게도 위 함수는 C++20 표준에 포함되지 않았다. 

 

다만, 2019년에 다음과 같이 제안되었으므로,

isocpp.org/files/papers/p1206r1.pdf

내가 40살이 되기 전엔 현업에서 to() 함수를 사용하는 날이 올 것이라 믿는다. 

 

Try 3 : begin(), end() 로 초기화


to() 함수의 대안은, 다음과 같이 begin(), end() 함수를 통한 초기화일 것이다.

std::vector<int> orin = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	
auto rng = std::ranges::views::all(orin);
std::vector<int> v(rng.begin(), rng.end());

위 예제를 VS에 입력해보면 잘 동작한다.

해결인가?

 

안타깝게도, 위 방법도 부족하다.

적어도, 모든 range 에 적용할 수는 없다.

 

rngbegin()Iterator 타입을 리턴하지만,

end()Iterator 타입을 리턴할수도, Sentinel 타입을 리턴할 수도 있기 때문이다.

즉, 두 함수의 리턴타입이 다를 수 있고, 이렇게 타입이 다른 경우엔

이터레이터를 통한 vector 초기화가 불가능한 것이다.

 

end()Sentinel 타입을 리턴할 수도 있다면, 위 예제는 어떻게 컴파일 되는 걸까?

위의 views::all()ranges::ref_view<std::vector<int>> 를 리턴한다.

ref_viewbegin(), end() 리턴 타입은

템플릿 파라미터로 전달받은 std::vector<int>begin(), end() 의 타입을 따라간다.

때문에 양쪽 모두 Iterator 타입이므로 컴파일이 가능하다.

 

위 예제를 다음과 같이 조금만 변경하면 컴파일이 불가능하다.

std::vector<int> orin = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	
auto rng = std::ranges::views::all(orin)
	| std::ranges::views::filter([](int a) { return a % 2 == 0; })
	| std::ranges::views::take(3);

std::cout << typeid(rng.begin()).name() << std::endl;
//class std::counted_iterator<class std::ranges::filter_view<...>::_Iterator>

std::cout << typeid(rng.end()).name() << std::endl;
//class std::ranges::take_view<...>::_Sentinel<0, 1>

//std::vector<int> v(rng.begin(), rng.end()); // compile error

 

그럼 어떻게 해야할까?

begin(), end() 의 리턴 타입을 모두 Iterator 로 만들 수는 없을까?

 

Try 4 : ranges::common_view


여기서 좋은 뉴스와 나쁜 뉴스가 있다.

 

좋은 뉴스는, C++ 20 에 begin()end() 의 리턴타입이 모두 Iterator 인 range 를 만들어주는

std::ranges::common_view 가 포함되어 있다는 점이다.

auto common = std::views::common(rng);
auto v = std::vector(common.begin(), common.end());

(단, std::ranges::basic_istream_view 등 일부 range에는 사용 불가)

 

나쁜 뉴스는, 위 view 가 아직 VS2019 에 구현되어 있지 않다는 점이다..

 

Try 5 : ranges::copy()


다른 대안은 없을까?

레퍼런스로 삼았던 글에서는, 아래와 같은 접근을 제안주셨다.

std::vector<std::ranges::range_value_t<decltype(r)>> v;

if constexpr(std::ranges::sized_range<decltype(r)>) {
    v.reserve(std::ranges::size(r));
}

std::ranges::copy(r, std::back_inserter(v));

range 의 타입을 decltyperanges::range_value_t 로 읽어서 벡터를 정의한 뒤

ranges::copy 로 원소를 복사하는 방법이다.

중간에 if constexpr 로, sized_range 컨셉을 만족하면 reserve() 까지 할 수 있어 효율적이다.

 

다만 이걸 매번 타이핑해서 쓰려면 현타가 올게 뻔하니, 

다음과 같이 함수화해서 ranges::common_view 혹은 ranges::to() 가 VS에 구현될 때까지 버텨보자.

template<class Rng>
requires std::ranges::range<Rng>
auto toVector(Rng& rng)
{
	std::vector<std::ranges::range_value_t<decltype(rng)>> v;

	if constexpr (std::ranges::sized_range<decltype(rng)>) {
		v.reserve(std::ranges::size(rng));
	}

	std::ranges::copy(rng, std::back_inserter(v));
	return v;
}

NRVO가 적용되므로 컨테이너가 복사되지 않는다.

 

 

Reference : timur.audio/how-to-make-a-container-from-a-c20-range

반응형

'C++' 카테고리의 다른 글

C++23 : stacktrace  (0) 2022.11.27
C++20 : std::ranges #2 - iota_view 만들기  (0) 2021.03.22
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