요약
지금은 방법이 없다.
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 에 적용할 수는 없다.
rng
의 begin()
은 Iterator
타입을 리턴하지만,
end()
는 Iterator
타입을 리턴할수도, Sentinel
타입을 리턴할 수도 있기 때문이다.
즉, 두 함수의 리턴타입이 다를 수 있고, 이렇게 타입이 다른 경우엔
이터레이터를 통한 vector
초기화가 불가능한 것이다.
end()
가 Sentinel
타입을 리턴할 수도 있다면, 위 예제는 어떻게 컴파일 되는 걸까?
위의 views::all()
은 ranges::ref_view<std::vector<int>>
를 리턴한다.
ref_view
의 begin()
, 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 의 타입을 decltype
및 ranges::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 |