[C++20] Range 연구

MIN·2025년 5월 8일

CPP20

목록 보기
3/8

Range

○ 원소에 대해 반복하는 타입 요구사항을 정의하는 Concept
○ begin()과 end()를 제공하는 데이터 구조라면 모두 Range라고 볼 수 있음

Rangerange 라이브러리에서 제공한다.
반복자 위에 추상화를 한 단계 더해서 반복자가 일치하지 않는 에러를 제거하고, 범위 어댑터를 통해 원소 시퀀스를 변환하거나 필터링하는 부가 기능도 제공한다.

범위 기반 알고리즘

○ 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 헤더파일 내용

profile
게임서버개발자(진)

0개의 댓글