----------------
title : 씹어먹는 C++ - <21. C++ 표준 라이브러리에서 자주 쓰이는 패턴 모음>
cat_title : 21. C++ 표준 라이브러리에서 자주 쓰이는 패턴 모음
path : /C++
publish_date : 2021-01-15
next_page : 335
----------------

![](/img/ChewingCpplogo.png)

안녕하세요 여러분. 이번 강의는 약간의 부록 형식으로 표준 라이브러리에서 자주 쓰이는 패턴들을 간단한 예제 코드들과 함께 정리해 놓은 글입니다. 시간이 날 때 마다 추가해보도록 하겠습니다. **특히 유용한 패턴들을 알고 계신다면 제보 부탁드립니다.**

### std::vector

#### 특정 원소로 벡터 초기화 하기

```cpp-formatted
// 0 을 10 개 가지는 벡터 생성
std::vector<int> vec(10, 0);
```

한국말로 생각하면 사실 인자의 순서가 헷갈릴 여지가 있는데 (저도 이 때문에 골치아픈 버그를 낸 적이 있습니다), 영어로 생각하면 편합니다. 예를 들어서 값이 0 인 원소 `10` 개로 초기화 하고 싶다면 *Ten zeros* 가 되므로 `(10, 0)` 순으로 쓰시면 됩니다.

#### 벡터 뒤에 원소 추가하기

간단히 `push_back` 을 사용하면 됩니다.

```cpp-formatted
std::vector<int> vec;
vec.push_back(3);
```

아래 처럼 `push_back` 에 객체를 생성하면서 전달해도 문제 없습니다.

```cpp-formatted
std::vector<std::unique_ptr<A>> vec;
vec.push_back(std::make_unique<A>());
```

이 과정에서 불필요한 복사나 생성이 이루어지지 않습니다. 만약에 복사 생성이 불가능한 원소의 경우 벡터에 이동 시키면서 `push_back` 할 수 있습니다.

```cpp-formatted
std::vector<std::unique_ptr<A>> vec;
auto a = std::make_unique<A>();
vec.push_back(std::move(a));
```

참고로 `emplace_back` 을 사용하는 것은 비추입니다. 옛날에 `emplace_back` 이 추가된 이유가 완벽한 전달(perfect forwarding) 을 통해서 불필요한 복사나 이동을 막기 위함이였는데 요즘의 컴파일러는 최적화가 잘 되어 있기 때문에 거의 대부분 `push_back` 을 사용했을 경우나 `emplace_back` 을 사용했을 경우 동일한 코드를 생성합니다.

그런데 `emplace_back` 의 가장 큰 문제점은 어떤 생성자가 호출되었는지 명확하지 않다는 점입니다.

```cpp-formatted
std::vector<std::vector<int>> vec;
vec.push_back(2000000);
```

예를 들어서 프로그래머가 실수로 위와 같이 2 차원 벡터에 `2000000` 을 `push_back` 하려고 했다고 봅시다. 위 코드의 경우 `push_back(2000000)` 를 하면 아래와 같은 오류가 발생합니다.

```compiler-warning
test.cc: In function ‘int main()’:
test.cc:7:18: error: no matching function for call to ‘std::vector<std::vector<int> >::push_back(int)’
    7 |   vec.push_back(2);
      |                  ^
In file included from /usr/include/c++/10/vector:67,
                 from test.cc:2:
/usr/include/c++/10/bits/stl_vector.h:1187:7: note: candidate: ‘void std::vector<_Tp, _Alloc>::push_back(const value_type&) [with _Tp = std::vector<int>; _Alloc = std::allocator<std::vector<int> >; std::vector<_Tp, _Alloc>::value_type = std::vector<int>]’
 1187 |       push_back(const value_type& __x)
      |       ^~~~~~~~~
/usr/include/c++/10/bits/stl_vector.h:1187:35: note:   no known conversion for argument 1 from ‘int’ to ‘const value_type&’ {aka ‘const std::vector<int>&’}
 1187 |       push_back(const value_type& __x)
      |                 ~~~~~~~~~~~~~~~~~~^~~
/usr/include/c++/10/bits/stl_vector.h:1203:7: note: candidate: ‘void std::vector<_Tp, _Alloc>::push_back(std::vector<_Tp, _Alloc>::value_type&&) [with _Tp = std::vector<int>; _Alloc = std::allocator<std::vector<int> >; std::vector<_Tp, _Alloc>::value_type = std::vector<int>]’
 1203 |       push_back(value_type&& __x)
      |       ^~~~~~~~~
/usr/include/c++/10/bits/stl_vector.h:1203:30: note:   no known conversion for argument 1 from ‘int’ to ‘std::vector<std::vector<int> >::value_type&&’ {aka ‘std::vector<int>&&’}
 1203 |       push_back(value_type&& __x)
      |                 ~~~~~~~~~~~~~^~~
```

당연하게도 `20000` 을 `std::vector<int>` 로 변환할 수 없기에 발생하는 일이지요.

반면에 `emplace_back` 을 할 경우

```cpp-formatted
std::vector<std::vector<int>> vec;
vec.emplace_back(2000000);
```

위 코드는 아무런 문제 없이 실행됩니다. 그럼 무슨 일이 일어난 것일까요? 이는 `emplace_back` 이 암묵적으로 `std::vector<int>(2000000)` 을 만들어서 넣은 것입니다! 사용자의 의도는 아마 그냥 `2000000` 이란 원소를 추가하고 싶었겠지만 `emplace_back` 을 사용했을 경우 크기가 `2000000` 인 벡터가 생성되어 추가됩니다. 상당히 비직관적이죠.

#### 다른 컨테이너 원소로 벡터 생성하기

C++ 의 모든 컨테이너들은 반복자의 시작과 끝을 인자로 받습니다. 이를 이용해서 컨테이너 사이의 변환을 쉽게 수행할 수 있습니다.

```cpp-formatted
std::unordered_set<int> s = {1, 2, 3, 4};

// s 의 원소를 가지고 벡터 생성
std::vector<int> vec(s.begin(), s.end());
```

참고로 위 경우 `vec` 에는 s 의 원소들이 **복사 생성** 됩니다. 만약에 복사가 불가능하고 이동 밖에 되지 않는 객체들은 어떨까요?

```cpp-formatted
#include <iostream>
#include <memory>
#include <vector>

int main() {
  std::vector<std::unique_ptr<int>> v1;

  v1.push_back(std::make_unique<int>(1));
  v1.push_back(std::make_unique<int>(2));
  v1.push_back(std::make_unique<int>(3));

  std::vector<std::unique_ptr<int>> v2(v1.begin(), v1.end());
}
```

컴파일 하였다면

```compiler-warning
In file included from /usr/include/c++/9/vector:66,
                 from test.cc:2:
/usr/include/c++/9/bits/stl_uninitialized.h: In instantiation of ‘_ForwardIterator std::uninitialized_copy(_InputIterator, _InputIterator, _ForwardIterator) [with _InputIterator = __gnu_cxx::__normal_iterator<std::unique_ptr<int>*, std::vector<std::unique_ptr<int> > >; _ForwardIterator = std::unique_ptr<int>*]’:
/usr/include/c++/9/bits/stl_uninitialized.h:307:37:   required from ‘_ForwardIterator std::__uninitialized_copy_a(_InputIterator, _InputIterator, _ForwardIterator, std::allocator<_Tp>&) [with _InputIterator = __gnu_cxx::__normal_iterator<std::unique_ptr<int>*, std::vector<std::unique_ptr<int> > >; _ForwardIterator = std::unique_ptr<int>*; _Tp = std::unique_ptr<int>]’
/usr/include/c++/9/bits/stl_vector.h:1582:33:   required from ‘void std::vector<_Tp, _Alloc>::_M_range_initialize(_ForwardIterator, _ForwardIterator, std::forward_iterator_tag) [with _ForwardIterator = __gnu_cxx::__normal_iterator<std::unique_ptr<int>*, std::vector<std::unique_ptr<int> > >; _Tp = std::unique_ptr<int>; _Alloc = std::allocator<std::unique_ptr<int> >]’
/usr/include/c++/9/bits/stl_vector.h:654:4:   required from ‘std::vector<_Tp, _Alloc>::vector(_InputIterator, _InputIterator, const allocator_type&) [with _InputIterator = __gnu_cxx::__normal_iterator<std::unique_ptr<int>*, std::vector<std::unique_ptr<int> > >; <template-parameter-2-2> = void; _Tp = std::unique_ptr<int>; _Alloc = std::allocator<std::unique_ptr<int> >; std::vector<_Tp, _Alloc>::allocator_type = std::allocator<std::unique_ptr<int> >]’
test.cc:12:61:   required from here
/usr/include/c++/9/bits/stl_uninitialized.h:127:72: error: static assertion failed: result type must be constructible from value type of input range
  127 |       static_assert(is_constructible<_ValueType2, decltype(*__first)>::value,
      |          
```

위와 같이 오류가 발생합니다. 당연히 `unique_ptr` 의 경우 복사가 불가능하기 때문이죠. 하지만 반복자로 하여금 복사 대신에 이동을 수행하게 할 수 있습니다. 바로 `std::make_move_iterator` 을 사용하면 됩니다.

```cpp-formatted
#include <iostream>
#include <memory>
#include <vector>

int main() {
  std::vector<std::unique_ptr<int>> v1;

  v1.push_back(std::make_unique<int>(1));
  v1.push_back(std::make_unique<int>(2));
  v1.push_back(std::make_unique<int>(3));

  std::vector<std::unique_ptr<int>> v2(std::make_move_iterator(v1.begin()),
                                       std::make_move_iterator(v1.end()));

  for (auto& i : v2) {
    std::cout << *i << std::endl;
  }
}
```

성공적으로 컴파일 하였다면

```exec
1
2
3
```

과 같이 나옵니다. `std::make_move_iterator` 은 입력 받은 반복자를 이동 반복자로 만들어줍니다. 이동 반복자의 경우 복사 대신에 이동 연산을 수행하므로 `unique_ptr` 와 같은 원소들을 이동시킬 수 있습니다.

참고로 굳이 벡터를 생성할 때 말고도 `insert` 할 때도 요긴하게 사용할 수 있습니다. 예를 들어서

```cpp-formatted
v2.insert(v2.end(), std::make_move_iterator(v1.begin()),
          std::make_move_iterator(v1.end()));
```

를 하게 되면 `v1` 안에 있는 원소 전체를 `v2` 뒤에 붙이게 됩니다.

참고로 이동 시켰다고 해서 기존 벡터의 원소가 사라지는 것은 아닙니다.

```cpp-formatted
#include <iostream>
#include <memory>
#include <vector>

int main() {
  std::vector<std::unique_ptr<int>> v1;

  v1.push_back(std::make_unique<int>(1));
  v1.push_back(std::make_unique<int>(2));
  v1.push_back(std::make_unique<int>(3));

  std::vector<std::unique_ptr<int>> v2(std::make_move_iterator(v1.begin()),
                                       std::make_move_iterator(v1.end()));

  std::cout << v1.size() << std::endl;
  std::cout << v2.size() << std::endl;
}
```

성공적으로 컴파일 하였다면

```exec
3
3
```

과 같이 `v1` 벡터의 원소 개수도 그대로 3 으로 나오죠. 그냥 `unique_ptr` 가 가리키는 원소 자체가 옮겨진 것이지, 벡터의 원소가 옮겨진 것은 아닙니다.

#### 벡터 안의 원소들 출력하기

물론 그냥 `for` 문으로 출력하면 되지만, C++ 11 에서 추가된 *range-for* 문법을 사용하면 더욱 간단하게 수행할 수 있습니다.

```cpp-formatted
#include <iostream>
#include <string>
#include <vector>

int main() {
  std::vector<std::string> v = {"hi", "hello", "abc"};
  for (const auto& s : v) {
    std::cout << s << std::endl;
  }
}
```

성공적으로 컴파일 하였다면

```exec
hi
hello
abc
```

와 같이 나옵니다.

만일 `fmt` 라이브러리를 사용하시면

```cpp-formatted
#include <fmt/ranges.h>

#include <string>
#include <vector>

int main() {
  std::vector<std::string> v = {"hi", "hello", "abc"};
  fmt::print("{}", v);
}
```

성공적으로 컴파일 하였다면

```exec
{"hi", "hello", "abc"}
```

와 같이 더욱 간단히 사용하실 수 있습니다. 참고로 `C++ 20` 에 `fmt` 의 `format` 함수가 포함되었으니 (아쉽게도 `print` 는 들어가지 못했습니다.) `std::cout << fmt::format("{}", v);` 와 같은 식으로 사용하시면 됩니다.

또 참신한 방법으로는 `std::copy` 와 `std::ostream_iterator` 를 사용하는 방법이 있습니다. 이 반복자의 경우 대입 연산을 수행하게 되면 대입된 값을 스트림에 출력합니다. 따라서 `std::copy` 로 벡터의 내용 전체를 이 반복자에 복사하게 되면 해당 내용이 전부 출력되겠죠.

```cpp-formatted
#include <iostream>
#include <iterator>
#include <string>
#include <vector>

int main() {
  std::vector<std::string> v = {"hi", "hello", "abc"};
  std::copy(v.begin(), v.end(),
            std::ostream_iterator<std::string>(std::cout, "\n"));
}
```

성공적으로 컴파일 하였다면

```exec
hi
hello
abc
```

와 같이 잘 나옵니다.

### 연관 컨테이너 관련

#### 맵이나 셋에서 조건을 만족하지 않는 키들 지우기

`std::vector` 와 같은 컨테이서에서는 `remove_if` 를 통해 원하지 않는 원소들을 솎아낸 뒤에 `erase` 를 호출해서 원소를 지워버릴 수 있습니다. 하지만 연관 컨테이너 (`map`, `set`, `unordered_map`, `unordered_set`) 의 경우 해당 작업이 불가능 합니다. (왜냐하면 `remove_if` 가 하는 일이 실질적으로 조건에 만족하는 원소들을 컨테이네 맨 뒤로 보내버리는 것이기 때문입니다. 하지만 연관 컨테이너의 경우 원소들 간의 순서를 마음대로 바꿀 수 없으니 아예 불가능합니다.)

```cpp-formatted
#include <algorithm>
#include <iostream>
#include <set>

int main() {
  std::set<int> s = {1, 2, 3, 4, 5, 6, 7};

  for (auto itr = s.begin(), last = s.end(); itr != last;) {
    // 짝수들만 지우자
    if (*itr % 2 == 0) {
      itr = s.erase(itr);
    } else {
      ++itr;
    }
  }

  for (auto& i : s) {
    std::cout << i << std::endl;
  }
}
```

성공적으로 컴파일 하였다면

```exec
1
3
5
7
```

와 같이 나옵니다.

연관 컨테이너의 `erase` 함수의 경우 C++ 11 부터 인자로 받은 반복자에 해당하는 원소를 지운 뒤에, 그 다음 원소를 가리키는 반복자를 리턴합니다. 따라서 위와 같이 `for` 문으로 모든 원소를 쭈르륵 순회하면서 지워버리면 됩니다.

참고로 이를 바탕으로 간단한 템플릿 버전을 만들어서 활용할 수 있습니다.

```cpp-formatted
#include <algorithm>
#include <iostream>
#include <unordered_map>

template <typename M, typename Pred>
void Erase(M& m, Pred pred) {
  for (auto itr = m.begin(), last = m.end(); itr != last;) {
    if (pred(*itr)) {
      itr = m.erase(itr);
    } else {
      ++itr;
    }
  }
}

int main() {
  std::unordered_map<int, int> m = {{1, 2}, {2, 3}, {3, 4}};

  Erase(m, [](const std::pair<int, int>& s) { return s.first % 2 == 0; });

  for (auto& [k, v] : m) {
    std::cout << k << ", " << v << std::endl;
  }
}
```

성공적으로 컴파일 하였다면

```exec
3, 4
1, 2
```

와 같이 키가 짝수인 애들만 지울 수 있습니다.

참고로 `C++ 20` 에는 `erase_if` 가 추가되어서 굳이 위 `Erase` 를 사용하지 않고도 동일한 작업을 수행할 수 있습니다.

#### 맵에서 키 (혹은 값) 추출하기

아쉽게도 표준 라이브러리를 사용해서 간단히 키를 추출하는 방법은 없습니다. `std::transform` 과 `std::back_inserter` 의 조합으로 할 수는 있지만 그리 직관적이지는 않습니다. 그냥 간단히 아래 처럼 `for` 문을 돌리는 수 밖에 없습니다.

```cpp-formatted
#include <algorithm>
#include <iostream>
#include <unordered_map>
#include <vector>

int main() {
  std::unordered_map<std::string, int> m = {{"a", 1}, {"b", 2}, {"c", 3}};

  std::vector<std::string> keys;
  for (const auto& [k, v] : m) {
    keys.push_back(k);
  }

  for (const auto& s : keys) {
    std::cout << s << std::endl;
  }
}
```

성공적으로 컴파일 하였다면

```exec
c
b
a
```

와 같이 나옵니다.

만약에 키나 값들을 복사해서 보관하는 것이 비싸기 때문에 그냥 그들을 가리키는 포인터들을 보관하는 것은 어떨까요? 맵의 원소들을 추가하거나 삭제한다면 다른 원소들의 위치가 옮겨질 수 도 있지 않을까요? 아닙니다. `std::map` 과 `std::unordred_map` 모두 다른 원소들을 추가하거나 삭제하더라도 기존 원소들의 레퍼런스는 유지되어야 한다고 하였습니다. ([참고](https://eel.is/c++draft/associative.reqmts#general-9)) \sidenote{The insert and emplace members **shall not** affect the validity of iterators and references to the container, and the erase members shall invalidate only iterators and references to the erased elements.}

따라서 아래와 같이 키들을 보관하는 벡터를 보관해도 괜찮습니다.

```cpp-formatted
#include <iostream>
#include <unordered_map>
#include <vector>

int main() {
  std::unordered_map<std::string, int> m = {{"a", 1}, {"b", 2}, {"c", 3}};

  std::vector<const std::string*> keys;
  for (const auto& [k, v] : m) {
    keys.push_back(&k);
  }

  for (const auto& addr : keys) {
    std::cout << *addr << " " << addr << std::endl;
  }

  std::cout << "---------------- " << std::endl;
  m["d"] = 4;
  m["e"] = 4;
  m["f"] = 4;
  m["g"] = 4;
  m["h"] = 4;

  for (const auto& [k, v] : m) {
    std::cout << k << " " << &k << std::endl;
  }
}
```

성공적으로 컴파일 하였다면

```exec
c 0x55606fa77f58
b 0x55606fa77f18
a 0x55606fa77ed8
---------------- 
g 0x55606fa78518
h 0x55606fa78558
b 0x55606fa77f18
d 0x55606fa78418
c 0x55606fa77f58
a 0x55606fa77ed8
f 0x55606fa784d8
e 0x55606fa78498
```

위와 같이 나옵니다. 위 처럼 원소들의 추가 후에도 `a, b, c` 문자열의 주소값이 바뀌지 않은 것을 확인할 수 있습니다.

### 문자열 관련

#### 문자열 안에서 특정 단어를 모두 찾아 바꾸기

흔히 우리가 말하는 *replace all* 과 같은 기능을 의미합니다. `std::string` 에는 `replace` 함수를 제공하고 있지만 이 함수는 특정 범위의 문자열을 다른 문자열로 바꾸는 간단한 함수 입니다. 하지만 이 함수와 `find` 를 이용해서 간단히 만들 수 있습니다.

```cpp-formatted
#include <iostream>
#include <string>
#include <string_view>

void ReplaceAll(std::string& src, std::string_view from, std::string_view to) {
  size_t current = 0;
  while ((current = src.find(from, current)) != std::string::npos) {
    src.replace(current, from.size(), to);
    current += to.size();
  }
}

int main() {
  std::string s =
    "I believe I can fly I believe I can fly I believe I can fly (woo)";
  std::cout << s << std::endl;

  ReplaceAll(s, "fly", "code");

  std::cout << s << std::endl;
}
```

성공적으로 컴파일 하였다면

```exec
I believe I can fly I believe I can fly I believe I can fly (woo)
I believe I can code I believe I can code I believe I can code (woo)
```

위와 같이 `"fly"` 를 모두 `"code"` 로 바꾼 것을 볼 수 있습니다. 참고로 위 `ReplaceAll` 은 마지막으로 치환된 위치를 기억하고 있기 때문에 빠르게 수행할 수 있습니다.

#### 보이어 무어 (Boyer-Moore) 알고리즘을 사용해서 문자열 검색 빠르게 수행하기

보이어 무어 알고리즘을 사용하면 일반적인 `find` 보다 훨씬 더 빠르게 문자열 검색을 수행할 수 있습니다. 문자열의 `find` 함수는 그냥 단순한 이중 `for` 문으로 구성되어 있다고 봐도 무방하기 때문에 원본 문자열의 길이가 $$N$$ 이고, 찾는 문자열의 길이가 $$M$$ 이라면 $$O(NM)$$ 으로 매우 느립니다. 반면에 보이어 무어 알고리즘의 경우 평균 시간 복잡도가 $$O(N)$$ 으로 월등히 빠릅니다.

```cpp-formatted
#include <algorithm>
#include <functional>
#include <iostream>
#include <string>

int main() {
  std::string s =
    "I believe I can fly I believe I can fly I believe I can fly (woo)";

  std::string needle = "believe";

  auto it =
    std::search(s.begin(), s.end(),
                std::boyer_moore_searcher(needle.begin(), needle.end()));

  if (it != s.end()) {
    std::cout << needle << " found at " << std::distance(s.begin(), it)
              << std::endl;
  } else {
    std::cout << needle << " not found " << std::endl;
  }
}
```

성공적으로 컴파일 하였다면

```exec
believe found at 2
```

와 같이 나옵니다. `std::search` 는 입력받는 범위 안에 원소들을 검색해서 찾았을 경우 해당 위치를 가리키는 반복자를 리턴합니다. 만일 단순하게 `find` 와 같은 방식으로 문자열을 검색하고 싶다면 그냥

```cpp-formatted
std::search(s.begin(), s.end(), needle.begin(), needle.end());
```

로 하면 됩니다만, 세 번째 인자로 어떠한 방식으로 검색할지 알려주는 `Searcher` 를 전달해주면 해당 알고리즘이 사용됩니다. 우리의 경우 `boyer_moore_searcher` 를 사용해서 검색하라고 알려줬기 때문에 해당 알고리즘이 실행됩니다.

#### 문자열 전체 대소문자 변경

만일 문자열 전체의 문자들에 대해 일괄적인 수정을 하고 싶다면 `std::transform` 을 사용하면 간단히 수행할 수 있습니다. 예를 들어서 모든 알파벳을 소문자로 바꾸려면

```cpp-formatted
std::transform(s.begin(), s.end(), s.begin(),
               [](char c) { return std::tolower(c); });
```

하면 됩니다.

#### 맨 앞 혹은 뒤에 문자들 제거 (파이썬의 lstrip, rstrip)

`string_view` 의 경우 `O(1)` 로 맨 앞 혹은 뒤의 문자들을 제거할 수 있습니다. 그 이유는 `string_view` 는 단순히 어떤 문자열의 view 이기 때문에 view 의 범위만을 바꿔주면 되기 때문이죠.

이를 통해서 `lstrip` 과 `rstrip` 을 빠르게 수행할 수 있습니다. 예를 들어서 `string_view` 에서 맨 앞에 연속된 공백 문자를 지우기 위해서는

```cpp-formatted
// lstrip
s.remove_prefix(std::min(s.find_first_not_of(" "), s.size()));
```

를 하면 맨 앞에 연속된 " " 를 지울 수 있습니다. 예를 들어서 `"   abc"` 는 `"abc"` 가 됩니다.

마찬가지로 `rstrip` 은

```cpp-formatted
s.remove_suffix(std::min(s.size() - s.find_last_not_of(" ") - 1, s.size()));
```

와 같이 하면 됩니다.