○ 원소에 대해 반복하는 타입 요구사항을 정의하는 Concept
○ begin()과 end()를 제공하는 데이터 구조라면 모두 Range라고 볼 수 있음
Range는 range 라이브러리에서 제공한다.
반복자 위에 추상화를 한 단계 더해서 반복자가 일치하지 않는 에러를 제거하고, 범위 어댑터를 통해 원소 시퀀스를 변환하거나 필터링하는 부가 기능도 제공한다.
○ std::ranges 네임스페이스에 속하며 동일한 기능의 범위 기반이 아닌 알고리즘과 동일한 헤더 파일에 정의
○ 이 알고리즘을 통해서 컴파일러가 알고리즘에 적합하지 않은 타입에 대해서 컴파일 에러를 정확하게 표시
int main()
{
vector<int> v1 = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
list<int> li = {1, 2, 3, 4, 5};
sort(v1.begin(), v1.end()); // 성공
std::ranges::sort(v1); // 동일한 결과로 성공
//sort(li.begin(), li.end()) // 빌드 에러
//std::ranges::sort(li); // 직접적으로 컴파일 에러 표시
}
위 vector 컨테이너를 사용해서 모든 원소를 정렬한다는 프로그래머의 의도를 명확히 드러낼 수 있다.
하지만 list 컨테이너를 사용하면 컴파일러가 바로 불편함을 드러낸다.
그 이유는 ranges::sort() 알고리즘은 임의 접근을 지원하는 범위를 지정해야 하는데 이를 제공하지 않기 때문에 에러 메시지를 출력하는 것이다.
ranges에는 여러 가지 범위를 검사할 수 있는 Concept가 존재한다.
이 부분을 확인해 보고 싶으면 여기에서 확인해 보길 바란다.
○ 범위 기반 알고리즘 중에서 프로젝션 콜백을 받아서 원소를 변환하는 알고리즘이 있음
다음 예시 코드를 보고 프로젝션을 살펴보자.
struct Knight
{
string name; // 사용될 프로젝션 변수
int id; // 사용될 프로젝션 변수
};
vector<Knight> knights =
{
{"Bong", 1},
{"Faker", 2},
{"Son", 3},
{"BTS", 4},
};
위 vector을 정렬하려고 ranges::sort을 사용하고 싶겠지만 아쉽게도 이 알고리즘을 사용할 수 없다.
왜냐하면 위 vector는 범위에 있는 원소를 비교할 방법이 없기 때문이다.
그러나 이름 혹은 숫자를 기준으로 정렬하고 싶다면 정렬 알고리즘에 프로젝션 함수를 지정해서 투영하면 된다.
// 프로젝션(projection)
std::ranges::sort(knights, {}, &Knight::name); // name을 기준으로 내림차순 정렬
std::ranges::sort(knights, std::ranges::greater(), &Knight::name); // name을 기준으로 오름차순 정렬
std::ranges::sort(knights, {}, &Knight::id); // id을 기준으로 내림차순 정렬
std::ranges::sort(knights, std::ranges::greater(), &Knight::id); // id을 기준으로 오름차순 정렬
프로젝션 매개변수는 세 번째로 지정하므로 원소를 비교하는 데 사용되는 두 번째 매개변수를 반드시 지정해야 된다.
○ 내부 범위에 있는 원소를 변환하거나 필터링하는 데 사용
○ 뷰를 조합하여 연산 파이프라인을 형성해서 범위에 적용 가능
뷰를 여러 개 연결하거나 조합해서 주어진 범위에 있는 원소에 대해 여러 연산을 수행하는 파이프라인을 구성할 수 있다.
조합 방법으로는 비트 단위 OR 연산자인 | 을 사용해서 합치면 된다.
그리고 뷰에서 꼭 알아야 하는 속성 3가지가 있다.
■ 지연 평가 -> 뷰만 구성해서는 내부 범위에 대해 연산을 수행할 수 없음
■ 비소유 -> 뷰는 어떤 원소도 소유하지 않음
■ 비변경 -> 뷰는 범위에 있는 데이터를 절대 변경하지 않음
다음 코드를 살펴보면서 뷰가 어떻게 사용되는지 살펴보자.
vector<int> v1 = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// v1 벡터의 요소에서 짝수를 찾고, 그 값들에 2을 곱해서 저장한다.
// 결과 : 4, 8, 12, 16, 20
auto results = v1 | std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * 2; });
// v2 벡터의 요소에서 짝수를 찾고, 그 값들에 2을 곱해서 저장한다.
// 그리고 앞에서부터 3개의 요소만 가져온다.
// 결과 : 4, 8, 12
auto results2 = v1 | std::views::filter([](int n) {return n % 2 == 0; })
| std::views::transform([](int n) { return n * 2; })
| std::views::take(3);
위 코드에서 사용된 views::filter 같은 코드를 범위 어댑터라고 한다.
범위 어댑터는 생성자를 호출하고 필요한 인수를 전달하는 방식으로 만들 수 있다.
하지만 생성자를 만들기보다는 주로 ranges::views 네임스페이스에 있는 범위 어댑터 객체와 비트 단위 OR 연산자를 조합하는 방식으로 만든다.
filter를 좀 더 살펴보자면, filter의 매개변수는 함수를 받아야 한다.
그래서 위 예시로 filter안에 람다식을 넣어서 사용한 걸 볼 수 있다.
사용자가 원하는 뷰가 없을 때는 직접 커스터마이징해서 뷰를 만들 수 있다.
다음 코드를 살펴보자.
// 클래스
// ranges::input_range 처음부터 끝까지 한 번 이상 반복할 수 있음
template<std::ranges::input_range Range>
requires std::ranges::view<Range> // Concept 사용 -> Range 템플릿은 반드시 view 이다.
class ContainerView : public std::ranges::view_interface<ContainerView<Range>>
{
// view_interface를 상속받으므로 view을 만들 수 있음
public:
ContainerView() = default;
constexpr ContainerView(Range r) : _range(std::move(r)), _begin(std::begin(r)), _end(std::end(r))
{
}
// begin과 end을 만들어서 범위 기반 알고리즘을 사용할 수 있게 만듦
constexpr auto begin() const { return _begin; }
constexpr auto end() const { return _end; }
private:
Range _range;
std::ranges::iterator_t<Range> _begin; // iterator_t -> 반복자를 사용할 수 있게 만들어줌
std::ranges::iterator_t<Range> _end;
};
// range를 view로 표준화 해주는 코드
template<class Range>
ContainerView(Range&& range) -> ContainerView<std::ranges::views::all_t<Range>>;
int main()
{
std::vector<int> myVec{ 5, 2, 3, 4, 1 };
auto myView = ContainerView(myVec);
ranges::sort(myView); // 정렬 알고리즘 사용 가능
for (auto n : myView)
{
cout << n << endl;
}
// 결과 : 1, 2, 3, 4, 5
}
필자가 Range을 사용하면서 느낀 점은 이 Range을 사용하면 프로그래머가 의도한 코드를 직관적으로 알 수 있게 만들어주는 힘이 있다. 그래서 이 부분을 극대화해서 사용하면 누구나 알아볼 수 있는 코드를 만들 수 있다. 그래서 필자는 자주 연습해서 사용해 볼 생각이다.
이런 부분을 많이 알수록 개발자 본인에게 유리하게 코딩을 할 수 있다.
● 전문가를 위한 C++(개정5판) P875~P886
● Inflearn [Rookiss][C++20 훑어보기]
● cppreference.com
● 마이크로소프트 본문 ranges 헤더파일 내용