C++20의 Ranges 라이브러리는 기존의 반복자 기반 알고리즘을 보다 간결하고 효율적으로 사용할 수 있도록 설계되었습니다. 기존 STL(Standard Template Library)과 달리 파이프라인 스타일을 제공하여 가독성을 높이고, 복잡한 컬렉션 처리를 쉽게 수행할 수 있습니다.
본 기사에서는 C++20 Ranges 라이브러리의 개념부터 시작하여, 이를 활용한 컬렉션 데이터 처리 최적화 기법을 다룹니다. 특히 std::views::filter
와 std::views::transform
과 같은 주요 기능을 중심으로 실전 예제와 성능 분석을 통해 Ranges가 코드 품질과 성능 개선에 미치는 영향을 설명합니다.
다음 내용을 통해 기존 STL 알고리즘과 Ranges의 차이점, 컬렉션 데이터 필터링 및 변환 방법, 그리고 최적화된 데이터 처리를 위한 실용적 활용법을 배워보겠습니다.
- Ranges 라이브러리란 무엇인가?
- Ranges를 사용한 반복자 개선
- 파이프라인 스타일을 활용한 데이터 변환
- Ranges 알고리즘 최적화 기법
- 1. Lazy Evaluation(지연 평가) 활용
- STL 방식: 불필요한 컨테이너 사용
- Ranges 방식: Lazy Evaluation 적용
- 2. 필터링 및 변환을 결합하여 최적화
- STL 방식: 중간 컨테이너 필요
- Ranges 방식: 파이프라인 스타일을 활용한 최적화
- 3. `std::views::take()`와 `std::views::drop()`으로 데이터 부분 선택
- 예제: 앞에서 5개의 요소만 선택
- 예제: 앞의 3개 요소를 건너뛰고 나머지를 선택
- 4. `std::views::reverse()`를 활용한 최적화
- STL 방식: 데이터 직접 수정
- Ranges 방식: 원본 데이터 유지
- 5. 벤치마크를 통한 성능 비교
- 결론
- Ranges와 기존 STL 컨테이너의 호환성
- 실전 예제: 데이터 필터링 및 변환
- Ranges를 활용한 성능 분석
- C++20 Ranges 활용 시 고려해야 할 사항
- 요약
Ranges 라이브러리란 무엇인가?
C++20의 Ranges 라이브러리는 기존 STL 알고리즘과 반복자(iterators)를 대체할 수 있는 더 간결하고 안전한 방식을 제공합니다. 기존 C++의 반복자 기반 알고리즘은 명시적인 반복자 사용과 복잡한 표현식으로 인해 코드 가독성이 떨어지는 문제가 있었습니다. Ranges는 이를 해결하기 위해 범위를 직접 표현하고 체이닝(Chaining) 스타일의 데이터 변환을 지원합니다.
기존 STL과 Ranges의 차이점
C++17까지는 std::vector
와 같은 컨테이너의 데이터를 처리할 때 명시적인 반복자와 함께 STL 알고리즘을 사용해야 했습니다. 예를 들어, 특정 조건을 만족하는 값을 필터링하는 경우 다음과 같은 코드를 작성해야 합니다.
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::vector<int> even_numbers;
std::copy_if(numbers.begin(), numbers.end(), std::back_inserter(even_numbers),
[](int n) { return n % 2 == 0; });
for (int n : even_numbers) {
std::cout << n << " ";
}
}
위 코드에서는 std::copy_if()
를 사용하여 짝수만 필터링했지만, 반복자의 명시적인 사용과 std::back_inserter
같은 부가적인 코드가 필요합니다.
C++20 Ranges의 코드 단순화
C++20의 Ranges 라이브러리를 사용하면 위와 동일한 작업을 훨씬 간결하고 직관적인 코드로 구현할 수 있습니다.
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto even_numbers = numbers | std::views::filter([](int n) { return n % 2 == 0; });
for (int n : even_numbers) {
std::cout << n << " ";
}
}
주요 차이점
- 반복자 명시 없이 범위(Pipeline) 스타일 사용:
numbers | std::views::filter(...)
형태로 데이터를 변환할 수 있습니다.|
연산자를 사용한 체이닝(Chaining) 방식으로 가독성이 뛰어납니다.
- Lazy Evaluation(지연 평가) 지원:
- Ranges는 필요할 때마다 데이터를 처리하여 불필요한 메모리 할당을 줄일 수 있습니다.
- 기존 STL 알고리즘은
std::vector
같은 컨테이너를 생성해야 했지만, Ranges는 바로 결과를 활용할 수 있습니다.
Ranges 라이브러리의 주요 장점
✔ 더 직관적인 코드: 파이프라인 스타일을 통해 명확한 데이터 흐름을 표현할 수 있습니다.
✔ 반복자 사용 감소: 명시적인 begin()
, end()
반복자를 사용하지 않아 코드가 간결해집니다.
✔ 퍼포먼스 최적화: Lazy Evaluation을 통해 메모리 사용량을 줄이고, 불필요한 계산을 최소화할 수 있습니다.
이제 Ranges를 활용하여 반복자 기반 코드를 어떻게 개선할 수 있는지 더 자세히 살펴보겠습니다.
Ranges를 사용한 반복자 개선
C++의 기존 STL(Standard Template Library)은 반복자(iterator)를 이용하여 컨테이너를 순회하는 방식이 일반적이었습니다. 하지만 반복자 기반 접근 방식은 명시적인 반복자 조작과 추가적인 코드 필요로 인해 가독성이 떨어지고 유지보수가 어렵다는 단점이 있습니다.
C++20 Ranges 라이브러리는 이러한 문제를 해결하고, 더 직관적이고 선언적인 방식으로 컬렉션을 처리할 수 있도록 설계되었습니다.
기존 STL 반복자 방식의 문제
STL의 std::vector
에서 홀수만 출력하는 코드를 살펴보겠습니다.
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 홀수 필터링
std::vector<int> odd_numbers;
std::copy_if(numbers.begin(), numbers.end(), std::back_inserter(odd_numbers),
[](int n) { return n % 2 != 0; });
// 결과 출력
for (auto it = odd_numbers.begin(); it != odd_numbers.end(); ++it) {
std::cout << *it << " ";
}
}
위 코드의 문제점:
- 명시적인 반복자(begin(), end()) 사용 → 코드가 장황해짐.
- 추가적인 컨테이너(odd_numbers) 필요 → 메모리 낭비 가능성.
- 반복자 조작 필요 → 실수할 가능성이 높아짐.
C++20 Ranges를 사용한 개선
C++20 Ranges를 활용하면 훨씬 간결하고 직관적인 코드로 같은 작업을 수행할 수 있습니다.
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 홀수 필터링 (파이프라인 스타일)
auto odd_numbers = numbers | std::views::filter([](int n) { return n % 2 != 0; });
// 결과 출력
for (int n : odd_numbers) {
std::cout << n << " ";
}
}
개선된 점
- 반복자 명시 없이 직관적인 표현 가능
|
연산자를 사용하여 체이닝 방식으로 데이터를 변환.
- 불필요한 컨테이너 생성 없이 Lazy Evaluation 적용
std::views::filter
는 필요한 데이터만 즉시 처리하기 때문에 메모리 사용량이 줄어듦.
- 코드의 가독성과 유지보수성이 향상됨
- 로직이 단순해지고, 표현 방식이 명확해짐.
반복자 대신 Ranges 사용 시 성능 비교
반복자 방식과 Ranges 방식의 성능을 비교하면, Ranges는 추가적인 메모리 할당 없이 데이터를 처리하기 때문에 더 효율적입니다.
방식 | 코드 길이 | 추가 컨테이너 필요 여부 | 실행 성능 | 가독성 |
---|---|---|---|---|
STL 반복자 | 길다 | 필요함 | 보통 | 낮음 |
Ranges | 짧다 | 필요 없음 (Lazy Evaluation) | 최적화 가능 | 높음 |
결론
C++20 Ranges 라이브러리를 활용하면 반복자 기반의 장황한 코드를 간결하게 줄이고, 불필요한 컨테이너 사용을 줄이며, 성능을 최적화할 수 있습니다.
다음으로, Ranges의 파이프라인 스타일을 활용한 데이터 변환 기법을 자세히 살펴보겠습니다.
파이프라인 스타일을 활용한 데이터 변환
C++20의 Ranges 라이브러리는 데이터를 처리하는 방식을 기존 STL보다 더 선언적이고 직관적인 파이프라인 스타일(Pipeline Style)로 개선했습니다. 이는 데이터 필터링, 변환 및 조합을 간결하게 수행할 수 있도록 도와줍니다.
기존 STL 방식의 문제점
기존 STL에서는 데이터를 변환할 때 std::transform()
과 같은 알고리즘을 사용해야 했으며, 이 과정에서 명시적인 반복자 조작과 별도의 컨테이너를 생성해야 하는 불편함이 있었습니다.
예제: 벡터의 각 요소를 제곱하여 저장
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::vector<int> squares(numbers.size());
std::transform(numbers.begin(), numbers.end(), squares.begin(),
[](int n) { return n * n; });
for (int n : squares) {
std::cout << n << " ";
}
}
단점:
squares
라는 추가적인 벡터가 필요함 → 메모리 낭비 가능성.std::transform()
을 사용할 때 반복자(begin(), end())를 명시적으로 사용해야 함.- 데이터 흐름이 직관적이지 않음 (원본 데이터를 변환 후 새로운 컨테이너로 복사해야 함).
C++20 Ranges를 사용한 파이프라인 스타일
C++20의 Ranges를 활용하면 데이터 변환을 더욱 간결하고 직관적인 방식으로 수행할 수 있습니다.
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5};
// 벡터의 각 요소를 제곱하는 파이프라인 스타일 코드
auto squares = numbers | std::views::transform([](int n) { return n * n; });
for (int n : squares) {
std::cout << n << " ";
}
}
개선된 점:
✅ std::views::transform()
을 사용해 명시적인 반복자 없이 바로 변환 수행
✅ Lazy Evaluation(지연 평가) 지원 → 불필요한 벡터 메모리 할당이 없음
✅ |
연산자를 사용한 체이닝(Chaining) 방식으로 코드 가독성 향상
필터링과 변환을 동시에 수행
C++20의 Ranges는 여러 연산을 연속적으로 적용할 수 있는 파이프라인 스타일을 지원합니다. 이를 활용하면 필터링과 변환을 한 줄의 코드로 쉽게 수행할 수 있습니다.
예제: 짝수만 필터링한 후 제곱하기
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto processed = numbers
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
for (int n : processed) {
std::cout << n << " ";
}
}
출력 결과:
4 16 36 64 100
설명:
std::views::filter([](int n) { return n % 2 == 0; })
→ 짝수만 필터링std::views::transform([](int n) { return n * n; })
→ 필터링된 숫자의 제곱값 변환- Lazy Evaluation 덕분에 추가 컨테이너 없이 바로 결과 생성
여러 개의 변환을 조합하기
다음 예제는 홀수를 필터링한 후, 두 배로 변환한 후, 마지막 3개 요소만 선택하는 방식입니다.
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto result = numbers
| std::views::filter([](int n) { return n % 2 != 0; }) // 홀수만 선택
| std::views::transform([](int n) { return n * 2; }) // 두 배 변환
| std::views::take(3); // 앞의 3개 요소만 선택
for (int n : result) {
std::cout << n << " ";
}
}
출력 결과:
2 6 10
설명:
std::views::filter([](int n) { return n % 2 != 0; })
→ 홀수만 선택std::views::transform([](int n) { return n * 2; })
→ 필터링된 숫자의 두 배 변환std::views::take(3)
→ 결과의 처음 3개 요소만 가져옴
결론
C++20 Ranges의 파이프라인 스타일을 활용하면:
✅ 반복자를 직접 조작하지 않아도 코드가 간결하고 직관적이다.
✅ 데이터 필터링과 변환을 한 줄의 코드로 손쉽게 수행할 수 있다.
✅ Lazy Evaluation을 활용해 불필요한 메모리 사용을 방지할 수 있다.
다음으로, Ranges 알고리즘을 활용한 성능 최적화 기법을 살펴보겠습니다.
Ranges 알고리즘 최적화 기법
C++20의 Ranges 라이브러리는 기존 STL 알고리즘을 개선하여 가독성을 높이고 불필요한 연산을 줄이는 최적화 기법을 제공합니다. 특히, std::views::filter
, std::views::transform
, std::views::take
, std::views::drop
등의 기능을 활용하면 데이터 처리 성능을 최적화할 수 있습니다.
이제 이러한 기능을 활용하여 성능을 최적화하는 방법을 살펴보겠습니다.
1. Lazy Evaluation(지연 평가) 활용
STL에서는 데이터를 변환하면 새로운 컨테이너에 저장하는 경우가 많지만, Ranges는 Lazy Evaluation(지연 평가)을 지원하여 필요한 요소만 처리하고 메모리 사용량을 줄일 수 있습니다.
STL 방식: 불필요한 컨테이너 사용
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::vector<int> squares;
std::transform(numbers.begin(), numbers.end(), std::back_inserter(squares),
[](int n) { return n * n; });
for (int n : squares) {
std::cout << n << " ";
}
}
문제점:
✅ squares
벡터를 추가적으로 생성해야 하므로 메모리 낭비 발생
✅ 모든 요소를 변환한 후 출력하므로 불필요한 연산이 수행됨
Ranges 방식: Lazy Evaluation 적용
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto squares = numbers | std::views::transform([](int n) { return n * n; });
for (int n : squares) {
std::cout << n << " ";
}
}
✅ 필요한 요소만 즉시 변환하여 출력
✅ 불필요한 벡터 할당 없이 바로 연산 수행
✅ 메모리 사용량 최소화
2. 필터링 및 변환을 결합하여 최적화
STL에서는 필터링 후 변환을 수행하려면 중간 컨테이너를 생성해야 하지만, Ranges에서는 체이닝 기법을 사용하여 한 번에 처리할 수 있습니다.
STL 방식: 중간 컨테이너 필요
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::vector<int> even_numbers;
std::vector<int> squared_even_numbers;
std::copy_if(numbers.begin(), numbers.end(), std::back_inserter(even_numbers),
[](int n) { return n % 2 == 0; });
std::transform(even_numbers.begin(), even_numbers.end(), std::back_inserter(squared_even_numbers),
[](int n) { return n * n; });
for (int n : squared_even_numbers) {
std::cout << n << " ";
}
}
✅ 필터링된 결과를 저장하기 위한 even_numbers
컨테이너가 필요
✅ 변환된 결과를 저장하기 위한 squared_even_numbers
컨테이너가 필요
Ranges 방식: 파이프라인 스타일을 활용한 최적화
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto result = numbers
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * n; });
for (int n : result) {
std::cout << n << " ";
}
}
✅ 중간 컨테이너 없이 한 번에 처리 가능
✅ Lazy Evaluation 적용으로 성능 최적화
✅ 가독성과 유지보수성 향상
3. `std::views::take()`와 `std::views::drop()`으로 데이터 부분 선택
데이터에서 특정 개수만 가져오거나, 특정 개수를 건너뛰고 싶은 경우 std::views::take()
와 std::views::drop()
을 사용하면 추가적인 컨테이너 없이 최적화된 처리가 가능합니다.
예제: 앞에서 5개의 요소만 선택
auto first_five = numbers | std::views::take(5);
✅ 불필요한 메모리 할당 없이 앞의 5개 요소만 가져옴
예제: 앞의 3개 요소를 건너뛰고 나머지를 선택
auto without_first_three = numbers | std::views::drop(3);
✅ 특정 개수를 건너뛰고 나머지를 바로 처리 가능
4. `std::views::reverse()`를 활용한 최적화
STL에서는 데이터를 뒤집기 위해 std::reverse()
를 사용해야 했지만, 이는 컨테이너 내부 데이터를 직접 변경해야 했습니다.
STL 방식: 데이터 직접 수정
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::reverse(numbers.begin(), numbers.end());
✅ 기존 데이터를 직접 수정 → 불변성이 깨질 위험
Ranges 방식: 원본 데이터 유지
auto reversed = numbers | std::views::reverse;
✅ 원본 데이터를 유지한 채 뒤집을 수 있음
✅ Lazy Evaluation 적용 가능
5. 벤치마크를 통한 성능 비교
간단한 벤치마크를 통해 STL과 Ranges의 성능 차이를 확인해 보면 다음과 같은 결과를 얻을 수 있습니다.
방식 | 메모리 사용량 | 실행 시간 | 코드 가독성 | 추가 컨테이너 |
---|---|---|---|---|
STL 알고리즘 | 높음 | 상대적으로 느림 | 보통 | 필요함 |
Ranges | 낮음 | 최적화 가능 | 뛰어남 | 불필요 |
✅ Ranges는 메모리 사용량을 줄이고 불필요한 연산을 최소화할 수 있어 더 효율적입니다.
결론
C++20의 Ranges 라이브러리를 활용하면 반복자 없이도 더욱 간결하고 최적화된 코드 작성이 가능합니다.
- Lazy Evaluation을 활용하여 불필요한 메모리 사용을 방지
- 체이닝 기법을 통해 코드를 간결하게 유지
std::views::filter
,std::views::transform
,std::views::take
,std::views::drop
등의 최적화된 함수를 적극 활용
다음으로, STL 컨테이너와 Ranges의 호환성에 대해 알아보겠습니다.
Ranges와 기존 STL 컨테이너의 호환성
C++20의 Ranges 라이브러리는 기존 STL 컨테이너(std::vector
, std::list
, std::deque
등)와 원활하게 호환됩니다. 그러나 몇 가지 제한 사항이 있으며, 적절한 변환 기법을 활용하면 더욱 효과적으로 사용할 수 있습니다.
본 장에서는 STL 컨테이너와 Ranges를 함께 사용하는 방법과 고려해야 할 사항을 살펴봅니다.
1. Ranges와 STL 컨테이너의 기본적인 호환성
STL 컨테이너(std::vector
, std::list
, std::deque
)는 기본적으로 Ranges와 함께 사용할 수 있습니다. 다만, Ranges는 기존 STL과 다르게 Lazy Evaluation(지연 평가)를 지원하기 때문에, 직접 컨테이너로 변환해야 할 때가 있습니다.
기본적인 Ranges와 STL 컨테이너 사용 예제
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 짝수만 필터링한 뷰 생성
auto evens = numbers | std::views::filter([](int n) { return n % 2 == 0; });
// 결과 출력
for (int n : evens) {
std::cout << n << " ";
}
}
✅ std::vector
의 데이터를 Ranges의 std::views::filter()
와 함께 사용 가능
✅ |
연산자를 사용하여 체이닝 방식으로 데이터를 처리
2. Ranges 결과를 STL 컨테이너로 변환하기
Ranges는 기본적으로 Lazy Evaluation을 사용하여 즉시 평가되지 않습니다. 만약 결과를 std::vector
와 같은 컨테이너에 저장하려면 std::ranges::to<>
또는 std::ranges::copy()
를 사용할 수 있습니다.
std::ranges::to<>
를 사용한 변환 (C++23 이상)
#include <iostream>
#include <vector>
#include <ranges>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 짝수만 필터링하고 vector로 변환
std::vector<int> evens = numbers | std::views::filter([](int n) { return n % 2 == 0; })
| std::ranges::to<std::vector>();
for (int n : evens) {
std::cout << n << " ";
}
}
✅ Lazy Evaluation을 해제하고 STL 컨테이너로 변환
✅ std::ranges::to<std::vector>()
는 C++23 이상에서 지원됨
3. C++20에서 `std::ranges::copy()`를 활용한 변환
C++20에서는 std::ranges::to<>
가 지원되지 않기 때문에, std::ranges::copy()
를 활용하여 변환해야 합니다.
#include <iostream>
#include <vector>
#include <ranges>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::vector<int> evens;
// 필터링된 결과를 STL 컨테이너에 저장
std::ranges::copy(
numbers | std::views::filter([](int n) { return n % 2 == 0; }),
std::back_inserter(evens)
);
for (int n : evens) {
std::cout << n << " ";
}
}
✅ std::ranges::copy()
와 std::back_inserter()
를 활용하여 STL 컨테이너에 저장 가능
✅ C++20에서도 STL 컨테이너와 Ranges를 쉽게 연동 가능
4. STL 컨테이너의 특정 동작과 Ranges의 차이
STL 컨테이너의 일부 기능은 Ranges와 호환되지 않거나, 예상과 다른 동작을 할 수 있습니다.
예를 들어, std::list
나 std::deque
같은 컨테이너에서 std::views::reverse()
를 사용할 경우, 정상적으로 동작하지 않을 수 있습니다.
std::views::reverse()
사용 시 주의할 점
#include <iostream>
#include <list>
#include <ranges>
int main() {
std::list<int> numbers = {1, 2, 3, 4, 5};
// std::views::reverse를 사용하면 undefined behavior 발생 가능
auto reversed = numbers | std::views::reverse;
for (int n : reversed) {
std::cout << n << " ";
}
}
🚨 std::list
는 std::views::reverse
를 직접 사용할 수 없음
🚨 std::list
는 연속적인 메모리 블록을 사용하지 않기 때문에, std::views::reverse
적용 시 문제가 발생할 수 있음
✅ 해결 방법:std::ranges::reverse_copy()
를 사용하여 변환된 결과를 std::vector
에 저장한 후 사용
#include <iostream>
#include <list>
#include <vector>
#include <ranges>
#include <algorithm>
int main() {
std::list<int> numbers = {1, 2, 3, 4, 5};
std::vector<int> reversed;
std::ranges::reverse_copy(numbers, std::back_inserter(reversed));
for (int n : reversed) {
std::cout << n << " ";
}
}
✅ std::ranges::reverse_copy()
를 사용하여 std::vector
로 변환 후 사용
✅ std::list
와 같은 비연속 메모리 컨테이너에서도 안전하게 사용 가능
5. STL 컨테이너에서 Ranges를 활용한 성능 개선
STL 컨테이너에서 Ranges를 사용하면 메모리 복사 비용을 줄이고, 성능을 최적화할 수 있습니다.
특히, std::views::drop()
과 std::views::take()
를 활용하면 불필요한 복사를 방지하고 성능을 향상시킬 수 있습니다.
예제: 벡터에서 앞 3개 요소를 건너뛰고 나머지를 사용
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto result = numbers | std::views::drop(3);
for (int n : result) {
std::cout << n << " ";
}
}
✅ 추가적인 std::vector
를 생성하지 않고 필요한 요소만 선택 가능
✅ Lazy Evaluation을 활용하여 성능 최적화
결론
C++20 Ranges는 기존 STL 컨테이너와 매우 유연하게 연동 가능하지만, 일부 컨테이너에서는 추가적인 변환이 필요할 수 있습니다.
✔ 기본 STL 컨테이너(std::vector
, std::deque
, std::list
)는 Ranges와 호환 가능
✔ Lazy Evaluation을 적극 활용하여 불필요한 메모리 사용을 줄일 수 있음
✔ C++20에서는 std::ranges::copy()
, C++23에서는 std::ranges::to<>
를 활용하여 STL 컨테이너로 변환 가능
✔ 비연속 메모리 컨테이너(std::list
)는 직접적으로 Ranges 일부 기능을 사용할 수 없음
다음으로, 실제 프로젝트에서 활용할 수 있는 실전 예제를 살펴보겠습니다.
실전 예제: 데이터 필터링 및 변환
이제까지 C++20 Ranges의 개념과 활용법을 살펴보았으므로, 실전에서 활용할 수 있는 구체적인 예제를 살펴보겠습니다.
다음 예제에서는 데이터 필터링과 변환을 결합하여 효율적인 데이터 처리를 수행하는 방법을 설명합니다.
1. 사용자 입력 데이터를 필터링 및 변환
사용자로부터 입력받은 숫자 목록에서 음수를 제외하고, 2배로 변환한 후, 가장 큰 5개 숫자만 출력하는 프로그램을 작성해보겠습니다.
#include <iostream>
#include <vector>
#include <ranges>
#include <algorithm>
int main() {
std::vector<int> numbers = {3, -1, 7, 10, -5, 2, 8, -3, 4, 9, -6};
// 음수 제거 -> 2배 변환 -> 가장 큰 5개 요소 선택
auto processed = numbers
| std::views::filter([](int n) { return n >= 0; })
| std::views::transform([](int n) { return n * 2; })
| std::views::take(5);
// 결과 출력
for (int n : processed) {
std::cout << n << " ";
}
}
출력 결과:
6 14 20 4 16
설명:
✔ std::views::filter([](int n) { return n >= 0; })
→ 음수 제외
✔ std::views::transform([](int n) { return n * 2; })
→ 모든 숫자를 2배로 변환
✔ std::views::take(5)
→ 변환된 숫자 중 앞 5개만 선택
2. 텍스트 데이터 처리: 대문자 변환 및 공백 제거
파일에서 읽어온 텍스트 데이터에서 공백을 제거하고, 모든 문자를 대문자로 변환하는 예제를 작성해보겠습니다.
#include <iostream>
#include <string>
#include <ranges>
#include <cctype>
int main() {
std::string text = "Hello World! Welcome to C++20 Ranges.";
// 공백 제거 -> 대문자로 변환
auto processed = text
| std::views::filter([](char c) { return !std::isspace(c); })
| std::views::transform([](char c) { return std::toupper(c); });
// 결과 출력
for (char c : processed) {
std::cout << c;
}
}
출력 결과:
HELLOWORLD!WELCOMETO C++20RANGES.
설명:
✔ std::views::filter([](char c) { return !std::isspace(c); })
→ 공백 제거
✔ std::views::transform([](char c) { return std::toupper(c); })
→ 대문자로 변환
✅ STL 반복자 없이 문자 필터링과 변환을 동시에 수행 가능!
3. 실시간 데이터 스트림 처리: 온도 센서 데이터 변환
온도 센서에서 실시간으로 데이터를 받아 특정 조건을 만족하는 값만 변환하는 예제입니다.
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<double> temperatures = {21.5, 19.8, 23.1, 25.4, 27.8, 22.0, 18.9};
// 22도 이상인 온도만 필터링 -> 화씨(Fahrenheit)로 변환
auto processed = temperatures
| std::views::filter([](double temp) { return temp >= 22.0; })
| std::views::transform([](double temp) { return temp * 9.0 / 5.0 + 32; });
// 결과 출력
for (double temp : processed) {
std::cout << temp << " ";
}
}
출력 결과:
73.58 77.72 82.04 71.6
설명:
✔ std::views::filter([](double temp) { return temp >= 22.0; })
→ 22도 이상 온도만 선택
✔ std::views::transform([](double temp) { return temp * 9.0 / 5.0 + 32; })
→ 섭씨를 화씨로 변환
✅ 필터링과 변환을 한 번에 수행하여 실시간 데이터 처리 가능!
4. JSON 데이터 가공: 특정 키 값만 추출
JSON 데이터에서 특정 키 값을 추출하는 경우에도 Ranges를 활용할 수 있습니다.
다음 예제에서는 사용자 목록에서 나이가 30 이상인 사용자만 추출합니다.
#include <iostream>
#include <vector>
#include <ranges>
#include <tuple>
int main() {
using Person = std::tuple<std::string, int>; // (이름, 나이)
std::vector<Person> people = {
{"Alice", 25},
{"Bob", 35},
{"Charlie", 30},
{"David", 28},
{"Eve", 40}
};
// 나이가 30 이상인 사람만 필터링 -> 이름만 추출
auto processed = people
| std::views::filter([](const Person& p) { return std::get<1>(p) >= 30; })
| std::views::transform([](const Person& p) { return std::get<0>(p); });
// 결과 출력
for (const auto& name : processed) {
std::cout << name << " ";
}
}
출력 결과:
Bob Charlie Eve
설명:
✔ std::views::filter([](const Person& p) { return std::get<1>(p) >= 30; })
→ 나이 30 이상인 사람만 선택
✔ std::views::transform([](const Person& p) { return std::get<0>(p); })
→ 이름만 추출
✅ STL의 std::vector<std::tuple<>>
과도 쉽게 호환 가능!
결론
C++20 Ranges를 사용하면 실제 응용 프로그램에서 반복자 없이 간결하게 데이터를 처리할 수 있습니다.
✔ 사용자 입력 데이터 필터링 및 변환
✔ 텍스트 데이터 변환 및 공백 제거
✔ 실시간 센서 데이터 변환 및 필터링
✔ JSON 형식의 데이터 가공 및 특정 키 값 추출
✅ STL과 호환되며, Lazy Evaluation을 활용하여 불필요한 연산을 줄일 수 있음
✅ 가독성이 높아 유지보수가 쉬운 코드 작성 가능
다음으로, Ranges를 활용한 성능 분석과 최적화 기법을 살펴보겠습니다.
Ranges를 활용한 성능 분석
C++20의 Ranges 라이브러리는 기존 STL 알고리즘과 비교하여 성능 최적화에 유리한 점이 많습니다. 특히 Lazy Evaluation(지연 평가)을 활용하면 불필요한 메모리 할당을 줄이고 연산 속도를 최적화할 수 있습니다. 본 장에서는 Ranges의 성능을 벤치마킹하고, 기존 STL 알고리즘과 비교하여 어떤 경우에 가장 효율적인지 분석합니다.
1. STL 알고리즘 vs. Ranges의 성능 비교
기존 STL 알고리즘과 Ranges를 비교하여 필터링 및 변환을 수행할 때의 성능 차이를 측정해보겠습니다.
벤치마크 대상
std::copy_if()
+std::transform()
(STL 방식)std::views::filter()
+std::views::transform()
(Ranges 방식)
STL 알고리즘 방식 (반복자 기반)
#include <iostream>
#include <vector>
#include <algorithm>
#include <chrono>
int main() {
std::vector<int> numbers(1'000'000);
std::iota(numbers.begin(), numbers.end(), 1);
std::vector<int> results;
auto start = std::chrono::high_resolution_clock::now();
std::copy_if(numbers.begin(), numbers.end(), std::back_inserter(results),
[](int n) { return n % 2 == 0; });
std::transform(results.begin(), results.end(), results.begin(),
[](int n) { return n * 2; });
auto end = std::chrono::high_resolution_clock::now();
std::cout << "STL 방식 실행 시간: "
<< std::chrono::duration<double>(end - start).count() << " 초\n";
}
✅ 필터링 후 변환을 수행하기 위해 추가적인 벡터(results
)가 필요
✅ 모든 데이터를 한 번에 처리하며, 중간 컨테이너가 필요함
Ranges 방식 (Lazy Evaluation 활용)
#include <iostream>
#include <vector>
#include <ranges>
#include <chrono>
int main() {
std::vector<int> numbers(1'000'000);
std::iota(numbers.begin(), numbers.end(), 1);
auto start = std::chrono::high_resolution_clock::now();
auto results = numbers
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * 2; });
int count = 0;
for (int n : results) {
if (++count > 1'000) break; // 일부 데이터만 사용하여 속도 측정
}
auto end = std::chrono::high_resolution_clock::now();
std::cout << "Ranges 방식 실행 시간: "
<< std::chrono::duration<double>(end - start).count() << " 초\n";
}
✅ 필터링과 변환을 한 번의 연산으로 수행 (Lazy Evaluation 적용)
✅ 중간 컨테이너 없이 필요한 데이터만 처리 가능
2. 성능 비교 결과
방식 | 실행 시간 | 추가 컨테이너 필요 여부 | 데이터 크기 증가 시 성능 변화 |
---|---|---|---|
STL 알고리즘 | 상대적으로 느림 | 필요함 (std::vector ) | 성능 저하 발생 |
Ranges | 상대적으로 빠름 | 필요 없음 (Lazy Evaluation) | 데이터 크기 증가 시에도 일정한 성능 유지 |
🚀 결과 분석:
- Ranges는 Lazy Evaluation 덕분에 필터링과 변환을 한 번에 수행할 수 있어 성능이 향상됨.
- STL 방식은 중간 컨테이너(
std::vector
)를 생성해야 하므로 추가적인 메모리 비용이 발생. - 데이터 크기가 커질수록 Ranges 방식의 성능 차이가 더욱 뚜렷해짐.
3. Ranges의 성능 최적화 기법
Ranges의 성능을 더욱 최적화할 수 있는 몇 가지 기법을 소개합니다.
1) std::views::take()
와 std::views::drop()
으로 불필요한 연산 최소화
필요한 데이터만 빠르게 가져올 수 있도록 std::views::take()
를 활용하면 성능을 최적화할 수 있습니다.
auto results = numbers
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * 2; })
| std::views::take(1000); // 처음 1000개 요소만 사용
✅ 필요한 만큼만 처리하므로 전체 데이터가 클 경우에도 빠르게 연산 가능
2) std::views::reverse()
를 활용하여 원본 데이터를 변경하지 않기
기존 STL에서는 데이터를 뒤집으려면 std::reverse()
를 사용해야 했지만, 이는 컨테이너의 데이터를 직접 변경해야 합니다.
반면, Ranges는 std::views::reverse()
를 사용하여 원본 데이터를 변경하지 않고도 순서를 뒤집을 수 있습니다.
auto reversed = numbers | std::views::reverse;
✅ 메모리 복사를 하지 않으므로 속도 최적화
3) std::views::unique()
를 활용한 중복 제거 최적화
기존 STL에서는 std::unique()
를 사용하면 벡터를 변경해야 하지만, Ranges에서는 std::views::unique()
를 사용하여 Lazy Evaluation 방식으로 처리할 수 있습니다.
auto unique_numbers = numbers | std::views::unique;
✅ 데이터를 직접 변경하지 않으며 중복된 값만 제외 가능
4. Ranges 사용 시 주의할 점
Ranges는 강력한 성능을 제공하지만, 몇 가지 주의해야 할 사항도 있습니다.
🚨 주의점 1: std::list
와 같은 비연속 메모리 컨테이너와의 호환성
std::views::reverse()
는std::list
에서 정상적으로 동작하지 않을 수 있음.- 해결 방법:
std::ranges::reverse_copy()
를 사용하여std::vector
로 변환한 후 사용.
🚨 주의점 2: Ranges는 무조건 빠르지 않음
- 작은 크기의 데이터에서는 기존 STL 알고리즘이 더 빠를 수 있음.
- 데이터 크기가 클수록 Ranges 방식의 성능 이점이 커짐.
결론
C++20의 Ranges를 활용하면 STL 반복자 기반 알고리즘보다 성능을 최적화할 수 있습니다.
- Lazy Evaluation을 통해 불필요한 메모리 사용을 줄이고, 성능을 향상시킬 수 있음.
- 필터링과 변환을 한 번에 수행하여 연산 속도를 개선 가능.
- 데이터 크기가 클수록 STL 알고리즘보다 성능 차이가 더욱 뚜렷하게 나타남.
✅ Ranges는 데이터 처리 성능을 최적화하는 강력한 도구이며, STL 알고리즘과 함께 사용하여 더욱 효과적으로 활용할 수 있습니다.
다음으로, C++20 Ranges 활용 시 고려해야 할 사항을 살펴보겠습니다.
C++20 Ranges 활용 시 고려해야 할 사항
C++20의 Ranges 라이브러리는 강력한 기능을 제공하지만, 모든 상황에서 최적의 선택이 될 수는 없습니다. Ranges를 사용할 때는 호환성, 성능, 코드 유지보수성 등의 요소를 고려해야 합니다.
본 장에서는 Ranges를 활용할 때 주의해야 할 점과 최적의 사용법을 정리합니다.
1. Ranges는 모든 STL 컨테이너에서 동작하지 않는다
STL의 주요 컨테이너(std::vector
, std::deque
, std::list
, std::map
, std::set
) 중 일부는 Ranges와 완벽하게 호환되지 않을 수 있습니다.
✅ 호환 가능한 컨테이너
std::vector
,std::deque
,std::string
등 연속적인 메모리 블록을 사용하는 컨테이너
🚨 주의가 필요한 컨테이너
std::list
,std::map
,std::set
등 비연속적인 메모리 구조를 사용하는 컨테이너std::views::reverse()
는std::list
에서 정상적으로 동작하지 않음.
#include <iostream>
#include <list>
#include <ranges>
int main() {
std::list<int> numbers = {1, 2, 3, 4, 5};
// std::views::reverse 사용 시 문제 발생 가능
auto reversed = numbers | std::views::reverse;
for (int n : reversed) {
std::cout << n << " ";
}
}
🚨 위 코드의 문제점:
std::list
는 연속적인 메모리 블록을 사용하지 않으므로std::views::reverse
를 직접 사용할 수 없음.
✅ 해결 방법:
#include <iostream>
#include <list>
#include <vector>
#include <ranges>
#include <algorithm>
int main() {
std::list<int> numbers = {1, 2, 3, 4, 5};
std::vector<int> reversed;
std::ranges::reverse_copy(numbers, std::back_inserter(reversed));
for (int n : reversed) {
std::cout << n << " ";
}
}
✔ 비연속 메모리 컨테이너는 std::ranges::reverse_copy()
를 사용하여 변환 후 처리
2. Lazy Evaluation이 항상 유리한 것은 아니다
Ranges는 기본적으로 Lazy Evaluation(지연 평가)을 사용하여 필요할 때만 데이터를 처리합니다.
그러나 일부 경우에는 즉시 평가(Eager Evaluation)가 더 적절할 수도 있습니다.
예제: Lazy Evaluation으로 인해 반복 평가가 발생하는 경우
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
auto result = numbers
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * 2; });
// 두 번 반복하면 필터링과 변환이 두 번 수행됨
for (int n : result) std::cout << n << " ";
std::cout << "\n";
for (int n : result) std::cout << n << " ";
}
🚨 문제점:
result
는 Lazy Evaluation을 사용하므로, 반복할 때마다 필터링과 변환이 다시 수행됨.- 큰 데이터셋에서 반복적으로 사용해야 하는 경우 즉시 평가가 더 적절할 수 있음.
✅ 해결 방법: 즉시 평가하여 저장하기
#include <vector>
#include <ranges>
#include <algorithm>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::vector<int> processed;
std::ranges::copy(
numbers | std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * 2; }),
std::back_inserter(processed)
);
for (int n : processed) std::cout << n << " ";
}
✔ 한 번만 평가하고 이후 반복 사용 가능
✔ Lazy Evaluation을 사용할 필요가 없을 때는 즉시 평가 방식 선택
3. `std::ranges::to<>` 변환 사용 가능 여부 (C++23 이상)
C++23에서는 std::ranges::to<>
를 사용하여 Ranges 데이터를 즉시 STL 컨테이너로 변환할 수 있습니다.
그러나 C++20에서는 지원되지 않으므로, std::ranges::copy()
를 사용해야 합니다.
✅ C++23에서 std::ranges::to<>
사용 예제
#include <iostream>
#include <vector>
#include <ranges>
int main() {
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::vector<int> processed = numbers
| std::views::filter([](int n) { return n % 2 == 0; })
| std::views::transform([](int n) { return n * 2; })
| std::ranges::to<std::vector>();
for (int n : processed) {
std::cout << n << " ";
}
}
🚨 주의: std::ranges::to<>
는 C++20에서는 지원되지 않음.
✅ C++20에서는 std::ranges::copy()
를 사용하여 컨테이너에 저장.
4. Ranges는 작은 데이터에서는 STL보다 빠르지 않을 수도 있다
일반적으로 데이터 크기가 크거나 복잡한 연산이 포함될 때 Ranges가 STL보다 성능이 우수합니다.
하지만 작은 데이터셋에서는 기존 STL 방식이 더 빠를 수도 있습니다.
데이터 크기 | STL 알고리즘 성능 | Ranges 성능 |
---|---|---|
작음 (1~1000개) | 빠름 | 비슷하거나 약간 느림 |
중간 (1000~1,000,000개) | 보통 | 최적화 가능 |
큼 (1,000,000개 이상) | 속도 저하 가능 | 성능 이점 명확 |
✅ 데이터 크기가 작은 경우 STL 알고리즘이 여전히 좋은 선택이 될 수 있음
✅ 큰 데이터셋에서는 Lazy Evaluation 덕분에 Ranges가 성능 이점을 가짐
결론
✔ STL 컨테이너와 호환성이 다르므로 주의가 필요함.
✔ Lazy Evaluation이 항상 유리한 것은 아니므로, 반복 사용 시 즉시 평가를 고려.
✔ C++23에서는 std::ranges::to<>
를 활용하면 변환이 편리하지만, C++20에서는 std::ranges::copy()
를 사용해야 함.
✔ 데이터 크기가 작은 경우 기존 STL 알고리즘이 더 빠를 수도 있음.
✅ C++20 Ranges는 성능 최적화와 코드 가독성 향상에 유용하지만, 적절한 사용법을 고려하여 적용하는 것이 중요합니다.
다음으로, 본 기사의 요약을 정리하겠습니다.
요약
본 기사에서는 C++20 Ranges 라이브러리를 활용하여 컬렉션 데이터 처리 성능을 향상시키는 방법을 다루었습니다. 기존 STL 반복자 기반 알고리즘과 비교하여 Ranges의 장점과 최적의 사용법을 분석하고, 성능 개선을 위한 다양한 기법을 소개했습니다.
📌 핵심 요약
✔ 기존 STL과 Ranges의 차이점
- Ranges는 반복자 없이 선언적 프로그래밍 스타일을 제공.
std::views::filter
,std::views::transform
등을 활용하여 파이프라인 스타일 코드 작성 가능.- Lazy Evaluation(지연 평가)를 통해 불필요한 연산과 메모리 사용 최소화.
✔ Ranges의 주요 기능과 활용
std::views::filter()
→ 조건에 맞는 데이터만 선택.std::views::transform()
→ 데이터 변환을 간결하게 수행.std::views::reverse()
→ 데이터 순서 변경을 효율적으로 처리.std::views::take(n)
/std::views::drop(n)
→ 특정 개수의 데이터만 선택 또는 제외.
✔ Ranges의 성능 분석
- 기존 STL 알고리즘보다 대용량 데이터 처리 시 성능이 우수.
- Lazy Evaluation 적용으로 불필요한 연산 감소.
- 단, 작은 데이터에서는 기존 STL 방식이 더 빠를 수도 있음.
✔ Ranges 활용 시 고려할 사항
std::list
,std::map
,std::set
같은 비연속 메모리 컨테이너는 일부 기능과 호환되지 않음.- Lazy Evaluation이 항상 최적은 아니므로, 반복 사용 시 즉시 평가 변환 필요.
- C++20에서는
std::ranges::copy()
를 사용하여 컨테이너 변환, C++23에서는std::ranges::to<>
지원.
🏆 C++20 Ranges를 활용하면?
✅ 반복자 없이 더욱 간결하고 가독성 높은 코드 작성 가능.
✅ 필터링, 변환, 조합을 효율적으로 수행하여 성능 최적화 가능.
✅ 대량의 데이터 처리에서 기존 STL 방식보다 성능 향상 가능.
C++20 Ranges를 활용하여 더욱 강력하고 최적화된 데이터 처리를 구현해 보세요! 🚀