C++20에 등장한 Range는 C#의 LINQ와 유사하다.
우선 아래와 같이 int형 벡터를 만들어 이 중 짝수원소들을 추출해 2를 곱한 새로운 벡터를 만든다고 가정해보자.
int main()
{
// C# LINQ 문법이랑 비슷하다?
vector<int> v1 = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
vector<int> v2;
// 짝수를 추출해서
for (int n : v1)
if (n % 2 == 0)
v2.push_back(n);
// 2를 곱해준다
for (int& n : v2)
n = n * 2;
return 0;
}
이렇게 벡터, 맵을 순회하면서 직접 연산할 수도 있지만 이는 이제 대체할 수 있는 오래된 스타일이 되어버렸다.
중간에서
std::for_each();
std::find_if();
std::any_of();
를 쓰기가 권장되기도 하지만 오늘 배울 range와 views를 쓰면 아래와 같이 쉽게 바꿀 수 있다.
#include <iostream>
#include <vector>
#include <map>
#include <ranges>
using namespace std;
int main()
{
vector<int> v1 = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
auto results = v1 | std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * 2; });
return 0;
}
간단하게 살펴보면 v1으로부터 filter를 사용해 추출해내는 이 하나가 일종의 파이프라인이다. 또 하나의 | 만 쓰는게 아니라 왼쪽에서부터 추가할 수 있다. 즉, filter로 짝수인 원소를 추출해 이를 대상으로 곱하기 2연산을 해준다. 그리고 연산할 함수는 람다로 넣어줘도 가능함을 알 수 있다.
또한 이를 쓰기 위해서는 ranges 라이브러리를 include해줘야한다.
std::views를 이용해 filter와 transform을 사용했는데 views는 ranges랑 무엇인지 알아보자.
Range : Range란 순회할 수 있는 아이템 그룹 (ex. STL Container)
View : Range에 대해서 적용할 수 있는 연산
정리하면 위와 같다. views에는 filter와 transform외에도 다양한 함수를 가지고 있는데 아래와 같다.
views, ranges의 함수들
std::views::all
std::ranges::filter_view / std::views::filter (조건 만족하는거 추출)
std::ranges::transform_view / std::views::transform (각 요소를 변환)
std::ranges::take_view / std::views::take (n개 요소를 추출)
std::ranges::take_while_view / std::views::take_while (조건 만족할 때까지 요소 추출)
std::ranges::drop_view / std::views::drop (n개 요소를 스킵)
std::ranges::drop_while_view / std::views::drop_while (조건 만족할 때까지 요소 스킵)
std::ranges::join_view / std::views::join (view 를 병합)
std::ranges::split_view / std::views::split (split)
std::ranges::reverse_view / std::views::reverse (역순서로 순회)
std::ranges::elements_view / std::views::elements (튜플의 n번째 요소를 대상으로 view 생성)
// 맵, 해쉬맵, unorderd_map 등의 key, value가 있는곳에서 사용가능한
std::ranges::keys_view / std::views::keys (pair-like value의 첫번째 요소를 대상으로 view 생성)
std::ranges::values_view / std::views::values (pair-like value의 두번째 요소를 대상으로 view 생성)
이를 좀 더 응용해보면
① takes
auto results2 = v1 | std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * 2; })
| std::views::take(3);
for (auto n : results2)
cout << n << " "; // 3개만 추출해 4,8,12가 나온다.
② sort
#include <vector>
#include <map>
#include <algorithm>
#include <ranges>
int main()
{
vector<int> v1 = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
//std::sort(v1.begin(), v1.end());
std::ranges::sort(v1);
}
algorithm에 있는 sort를 범위를 지정해줘야하는데 ranges에 있는 sort는 범위를 지정해주지 않아도 되어 조금 더 깔끔하다.
또 rages::sort도 두번째 인자는 Predicate(조건)를 받아 less가 기본이며 greater를 넣을 수도 있다. 세번째 인자로는 Projection을 받아서 어떤 원소를 대상으로 정렬을 할지 지정할 수 있다. 이를 좀더 응용해보자.
#include <vector>
#include <map>
#include <algorithm>
#include <ranges>
int main()
{
struct Knight
{
std::string name;
int id;
};
vector<Knight> knights =
{
{ "Rookiss", 1},
{ "Faker", 2},
{ "Dopa", 3},
{ "Deft", 4},
};
std::ranges::sort(knights, {}, &Knight::name); // ascending by name, Deft, Dofa, Faker,Rookis
std::ranges::sort(knights, std::ranges::greater(), &Knight::name); // descending by name
std::ranges::sort(knights, {}, &Knight::id); // ascending by id
std::ranges::sort(knights, std::ranges::greater(), &Knight::id); // ascending by id
return 0;
}
위처럼 sort에 두, 세번째 인자를 채워줌으로서 Knight 객체 배열 knights를 name원소 기준, id기준 내림차순/오름차순 정렬할 수 있다.
③ map을 대상으로 사용하기
#include <iostream>
#include <map>
#include <algorithm>
#include <ranges>
using namespace std;
int main()
map<string, int> m =
{
{ "Rookiss", 1},
{ "Faker", 2},
{ "Dopa", 3},
{ "Deft", 4},
};
for (const auto& name : std::views::keys(m) | std::views::reverse)
cout << name << endl;
return 0;
}
m의 키들 역순으로 순회하게 된다.
Deft, Dopa, Faker, Rookis
④ 0~100 사이의 숫자중 소수인 5개의 숫자를 추출하라?
int main()
// 소수판별함수
auto isPrime = [](int num)
{
if (num <= 1)
return false;
for (int n = 2; n*n <= num; n++)
if (num % n == 0)
return false;
return true;
};
std::vector<int> v3;
// std::views::iota(a, b) : a부터 시작해서 1씩 증가 b개를 만들어줌
for (int n : std::views::iota(0, 100) | std::views::filter(isPrime) | std::views::take(5))
{
v3.push_back(n);
}
return 0;
}
우선 std::views::iota(a, b) : a부터 시작해서 1씩 증가 b개를 만들어 주는 함수를 이용해 0부터 100까지 수를 만든 다음에,
소수를 판별하는 isPrime을 람다함수로 만들어서(에라스토테네스이군) filter로 넣어주고 이 중 5개를 선별하는 take로 마무리한다. 이 값을 순회하며 n으로 받아서 v3에 넣는다.
⑤ 커스텀 뷰 (std::ranges::view_interface)
이미 std에 있는 view가 아닌 새로운 방식으로 view를 만들어서 사용하고자 할 때 어떻게 쓰면 되는지 살펴보자.
#include <iostream>
using namespace std;
#include <list>
#include <vector>
#include <map>
#include <algorithm>
#include <ranges>
#include <concepts>
template<std::ranges::input_range Range>
requires std::ranges::view<Range>
class ContainerView : public std::ranges::view_interface<ContainerView<Range>>
{
public:
ContainerView() = default;
constexpr ContainerView(Range r) : _range(std::move(r)), _begin(std::begin(r)), _end(std::end(r))
{
}
constexpr auto begin() const { return _begin; }
constexpr auto end() const { return _end; }
private:
Range _range;
std::ranges::iterator_t<Range> _begin;
std::ranges::iterator_t<Range> _end;
};
template<typename Range>
ContainerView(Range&& range) -> ContainerView<std::ranges::views::all_t<Range>>;
int main()
{
// 커스텀 뷰 (std::ranges::view_interface)
std::vector<int> myVec{1,2,3,4,5};
auto myView = ContainerView(myVec);
for (auto n : myView)
{
cout << n << endl;
}
}
std::ranges::view_interface를 상속받아서 ContainerView클래스를 만들었다. 이때 Range타입을 모두 받기 위해 템플릿을 사용하는데 Ranges를 받도록 concept으로 조건을 받았다.
그리고 iterator를 사용해 순회할 수 있도록 begin, end를 지원해줘야한다.
그리고 public으로 ContainerView의 생성자를 만들어주는데 선처리 영역에서 _range, _begin, _end를 채워주도록 했다.
마찬가지로 public으로 begin()과 end()에서 이를 가져올 수 있도록 한다.
이를 응용해서 보면 위와 같이 myVec이란 이름으로 일반 벡터를 만들었다가 그대로 방금 만든 ContainerView커스텀 뷰에 적용해서 쓸 수 있다. 즉, 벡터를 view로 만드는 헬퍼클래스같은 역할을 ContainerView가 하고 있다.
https://learn.microsoft.com/ko-kr/cpp/standard-library/ranges?view=msvc-170
<ranges>
STL(표준 템플릿 라이브러리) 범위에 대한 개요를 확인합니다.
learn.microsoft.com
이렇게 살펴봤지만 ranges와 view를 이용해 for를 간단하게 쓰는 방법으로만 보는게 좋을 듯하다.
'[C++]' 카테고리의 다른 글
가상함수테이블과 가상함수테이블포인터 정리 (0) | 2025.01.02 |
---|---|
[C++ 20] Coroutine (0) | 2024.12.11 |
[C++ 20] Module (1) | 2024.11.13 |
Modern C++ #9 전달 참조(forwarding reference) (0) | 2024.10.22 |
Modern C++ #8 오른값 참조(rvalue reference)와 std::move (0) | 2024.10.22 |