성능과 가독성이라는 두 가지 측면에서 STL 알고리즘을 사용한다. 다 다루진 않습니다~
STL(Standard Template Library)는 일종의 데이터 타입을 모은 세트이며, 컨테이너이자 표준 C++에 포함된 알고리즘이다. STL의 알고리즘은 컨테이너가 아니라 반복자
에 대해 동작한다.
모든 알고리즘은 하나의 반복자 쌍이 필요하며, 범위에서 첫 번째 요소를 가리키는 지점과 마지막 요소의 끝 부분을 가리키는 두 번째 지점을 말한다.
auto vec = std::vectpr<std::string>{
"a", "b", "c", "d"};
auto first = vec.begin();
auto last = vec.end();
보다시피 last는 "d" 요소의 뒷부분
을 가리키고 있다.
STL 알고리즘은 특정 범위의 요소를 수정
만 할 수 있다. 해당 요소는 컨테이너에 추가 , 삭제 되지 않는다.
예를 들어 std::remove()나 std::unique()의 경우 실제로는 컨테이너에서 요소를 삭제하는게 아니라,
제거하려는 요소가 뒷부분에 배치되도록 재정렬하는 것이다.
auto vec = std::vector<int>{1, 2, 3, 4};
auto new_en = std::remove(
vec.begin(), vec.end(), 3
);
실제로 제거는 vec.erase(new_end, vec_end());로 실행
출력 반복자에 데이터를 기록하는 알고리즘은 출력하려면 이미 할당된 데이터가 필요하다
.
컨테이너가 비었는데 출력을 위한 알고리즘을 사용하면 그 프로그램은 크래시(crash)
된다. 고장난다는 거지
이런 문제를 막으려면 둘 중 하나를 수행해야 한다.
struct Month {
// 검색할 때 사용하는 동일 여부의 판단 동작
auto operator==(const Month& m) const {
return height_ == m.height_; }
int height_{};
};
// std::find는 operator==를 사용
auto birth_month = *std::find(month.begin(), month.end(), Month{9});
모드 알고리즘은 요소를 이동시킬 때 std::swap이나 std::move를 사용하지만 이동 생성자와 이동 할당이 noexcept
로 명시돼 있는 경우에만 해당된다.
직접 for 반복문을 만들기보다는 알고리즘을 사용하는 것이 좋다. 가독성이 좋고 오류가 발생할 가능성도 줄며, 코드를 다시 찾아 수정하고 최적화하기 쉽다.
※ for문으로 가득한 코드 베이스는 모든 알고리즘이 비슷~하게 보여서 읽기가 어렵다.
앞에서 STL 알고리즘 라이브러리는 한 쌍의 반복자 파라미터가 필요하다고 설명했다. 그래서 살짝 복잡한 구문을 갖고 있다. 하지만 범위 라이브러리는 반복자 대신 범위를 파라미터로 사용한다.
반복자로 동작하는 STL 알고리즘 | 컨테이너로 동작하는 범위 라이브러리 |
---|---|
std::sort(a.begin(), a.end()); std::count(a.begin(), a.end(), 12); | ranges::sor(a); ranges::count(a, 12); |
이처럼 보다 간단해진다.
범위 라이브러리는 세 가지 유형의 동작인 액션
, 뷰
, 알고리즘
으로 구성돼 있다
근데 핵심은 뷰! 뷰를 포함하여 범위 라이브러리의 세 가지 유형을 알아보자.
※ 예를 들어 이런 게 있다.
ranges::action::sort; // 이건 액션
ranges::view::unique; // 이건 뷰
입출력을 위해 컨테이너를 사용하고, 새롭게 수정된 컨테이너를 반환한다.
액션은 컨테이너의 크기를 변경한다.
1.2 에서 std::remove
를 하더라도 실제로 컨테이너의 요소가 지워진 건 아니라고 했다.
반면, 액션은 ranges::action::remove
를 할 경우 컨테이너에서 요소를 제거
한 작아진 컨테이너를 반환한다. unique도 마찬가지.
액션은 데이터를 변경하기 때문에 뷰에서 동작하지 않는다.
액션을 사용해서 컨테이너를 변경하려면 다음 중 하나를 선택해야 한다.
구문 접근법 | |
---|---|
준비 단계 : 코드 초기화 | namespace ra = ranges::action; auto get(){return std::vector{1, 3, 5, 7};} auto above_5 = [](auto v){ return v >= 5; }; |
get()을 직접 사용 | auto vals = get() | ra::remove_if(above_5); // vals는 "1, 3" |
std::move로 만든 r-value | auto vals = get(); vals = std::move(vals) | ra::remove_if(above_5); // vals는 "1, 3" |
|= 연산자를 사용한 변경 컨테이너 | auto vals = get(); vals |= ra:: remove_if(above_5); // vals는 "1, 3" |
액션은 입력 컨테이너로 작업하고 변경된 버전을 반환하는 반면에, 뷰는 반복 연산 시점에 변경된 컨테이너처럼 보이는 프록시 뷰
를 반환할 뿐이다.
컨테이너는 전혀 변경되지 않으며, 모든 처리 과정은 반복자 내에서 수행된다.
액션은 컨테이너를 변환하므로 타입을 변환할 수 없지만, 뷰는 어떤 타입의 뷰로도 변환할 수 있다.
namespace ra = ranges::action;
namespace rv = ranges::view;
auto get_number() { return std::vector<int>{1,3,5,7}; }
// 액션은 타입을 변환할 수 없다.
auto strings = get_number() | ra::transform([](auto v){
return std::to_string(v); // 컴파일되지 않는다!
});
// 뷰는 타입을 변환할 수 있다.
auto ints = get_numbers();
auto str_view = ints | rv::transform([](auto v){
return std::to_string(v);
});
뷰나 변경된 컨테이너를 반환하지 않는 범위 라이브러리의 알고리즘은 단순히 알고리즘으로만 참조된다. ranges::count 나 ranges::any_of 등이 해당한다. 얘네는 반복자 한 쌍 대신 범위를 사용하는 경우를 제외하고는 STL에서 변형이 없는 알고리즘으로서 정확하게 동작한다.