[C++ 공부] STL 알고리즘

Yujin Lee·2022년 2월 23일
0

Cpp_Study

목록 보기
7/8
post-thumbnail

성능과 가독성이라는 두 가지 측면에서 STL 알고리즘을 사용한다. 다 다루진 않습니다~

1. STL 알고리즘

STL(Standard Template Library)는 일종의 데이터 타입을 모은 세트이며, 컨테이너이자 표준 C++에 포함된 알고리즘이다. STL의 알고리즘은 컨테이너가 아니라 반복자 에 대해 동작한다.


1.1 처음과 마지막 요소를 가리키는 범위에 대한 반복자

begin(), end()

모든 알고리즘은 하나의 반복자 쌍이 필요하며, 범위에서 첫 번째 요소를 가리키는 지점과 마지막 요소의 끝 부분을 가리키는 두 번째 지점을 말한다.

auto vec = std::vectpr<std::string>{
	"a", "b", "c", "d"};
auto first = vec.begin();
auto last = vec.end();

보다시피 last는 "d" 요소의 뒷부분을 가리키고 있다.


1.2 추가, 삭제가 아니라 '재정렬'

std::remove(), std::unique(), ...

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());로 실행


1.3 할당된 데이터로 출력하는 알고리즘

std::copy(), std::transform(), ...

출력 반복자에 데이터를 기록하는 알고리즘은 출력하려면 이미 할당된 데이터가 필요하다.
컨테이너가 비었는데 출력을 위한 알고리즘을 사용하면 그 프로그램은 크래시(crash)된다. 고장난다는 거지

이런 문제를 막으려면 둘 중 하나를 수행해야 한다.

  1. 결과 컨테이너에 필요한 크기만큼 사전 할당 한다.
  2. 반복 연산 과정에 요소를 컨테이너에 추가하는 insert 반복자를 사용한다.

1.4 비교 연산을 위한 알고리즘

std::find(), std::max_element, ...

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});

1.5 이동 연산자가 필요한 알고리즘

std::swap(), std::move(), ...

모드 알고리즘은 요소를 이동시킬 때 std::swap이나 std::move를 사용하지만 이동 생성자와 이동 할당이 noexcept로 명시돼 있는 경우에만 해당된다.



2. for문과 비교했을 때 STL 알고리즘의 장점

직접 for 반복문을 만들기보다는 알고리즘을 사용하는 것이 좋다. 가독성이 좋고 오류가 발생할 가능성도 줄며, 코드를 다시 찾아 수정하고 최적화하기 쉽다.
※ for문으로 가득한 코드 베이스는 모든 알고리즘이 비슷~하게 보여서 읽기가 어렵다.



3. 범위 라이브러리

앞에서 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;  // 이건 뷰

3.1 액션

입출력을 위해 컨테이너를 사용하고, 새롭게 수정된 컨테이너를 반환한다.

액션은 컨테이너의 크기를 변경한다.
1.2 에서 std::remove를 하더라도 실제로 컨테이너의 요소가 지워진 건 아니라고 했다.
반면, 액션은 ranges::action::remove를 할 경우 컨테이너에서 요소를 제거한 작아진 컨테이너를 반환한다. unique도 마찬가지.

액션은 데이터를 변경하기 때문에 뷰에서 동작하지 않는다.

액션을 사용해서 컨테이너를 변경하려면 다음 중 하나를 선택해야 한다.

  • 컨테이너를 r-value로 제공한다. 즉, 함수나 std::move()를 통해 범위가 반환된다.
  • 액션을 컨테이너를 변경하는 | 대신 |=를 사용해 초기화한다.
구문 접근법
준비 단계 : 코드 초기화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-valueauto 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"

3.2 뷰

액션은 입력 컨테이너로 작업하고 변경된 버전을 반환하는 반면에, 뷰는 반복 연산 시점에 변경된 컨테이너처럼 보이는 프록시 뷰를 반환할 뿐이다.

컨테이너는 전혀 변경되지 않으며, 모든 처리 과정은 반복자 내에서 수행된다.

액션은 컨테이너를 변환하므로 타입을 변환할 수 없지만, 뷰는 어떤 타입의 뷰로도 변환할 수 있다.
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);
});

3.3 알고리즘

뷰나 변경된 컨테이너를 반환하지 않는 범위 라이브러리의 알고리즘은 단순히 알고리즘으로만 참조된다. ranges::count 나 ranges::any_of 등이 해당한다. 얘네는 반복자 한 쌍 대신 범위를 사용하는 경우를 제외하고는 STL에서 변형이 없는 알고리즘으로서 정확하게 동작한다.

profile
I can be your Genie🧞‍♀️ How ‘bout Aladdin? 🧞‍♂️

0개의 댓글