모두의 코드
씹어먹는 C++ - <21. C++ 표준 라이브러리에서 자주 쓰이는 패턴 모음>

작성일 : 2021-01-15 이 글은 1025 번 읽혔습니다.

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

std::vector

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

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

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

벡터 뒤에 원소 추가하기

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

std::vector<int> vec;
vec.push_back(3);

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

std::vector<std::unique_ptr<A>> vec;
vec.push_back(std::make_unique<A>());

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

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 의 가장 큰 문제점은 어떤 생성자가 호출되었는지 명확하지 않다는 점입니다.

std::vector<std::vector<int>> vec;
vec.push_back(2000000);

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

컴파일 오류

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)
      |                 ~~~~~~~~~~~~~^~~

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

반면에 emplace_back 을 할 경우

std::vector<std::vector<int>> vec;
vec.emplace_back(2000000);

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

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

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

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

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

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

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

컴파일 하였다면

컴파일 오류

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 을 사용하면 됩니다.

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

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

실행 결과

1
2
3

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

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

v2.insert(v2.end(), std::make_move_iterator(v1.begin()),
          std::make_move_iterator(v1.end()));

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

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

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

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

실행 결과

3
3

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

벡터 안의 원소들 출력하기

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

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

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

실행 결과

hi
hello
abc

와 같이 나옵니다.

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

#include <fmt/ranges.h>

#include <string>
#include <vector>

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

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

실행 결과

{"hi", "hello", "abc"}

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

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

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

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

실행 결과

hi
hello
abc

와 같이 잘 나옵니다.

연관 컨테이너 관련

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

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

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

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

실행 결과

1
3
5
7

와 같이 나옵니다.

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

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

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

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

실행 결과

3, 4
1, 2

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

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

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

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

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

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

실행 결과

c
b
a

와 같이 나옵니다.

만약에 키나 값들을 복사해서 보관하는 것이 비싸기 때문에 그냥 그들을 가리키는 포인터들을 보관하는 것은 어떨까요? 맵의 원소들을 추가하거나 삭제한다면 다른 원소들의 위치가 옮겨질 수 도 있지 않을까요? 아닙니다. std::mapstd::unordred_map 모두 다른 원소들을 추가하거나 삭제하더라도 기존 원소들의 레퍼런스는 유지되어야 한다고 하였습니다. (참고)

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

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

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

실행 결과

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 를 이용해서 간단히 만들 수 있습니다.

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

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

실행 결과

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)$ 으로 월등히 빠릅니다.

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

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

실행 결과

believe found at 2

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

std::search(s.begin(), s.end(), needle.begin(), needle.end());

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

문자열 전체 대소문자 변경

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

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

하면 됩니다.

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

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

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

// lstrip
s.remove_prefix(std::min(s.find_first_not_of(" "), s.size()));

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

마찬가지로 rstrip

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

와 같이 하면 됩니다.

첫 댓글을 달아주세요!
프로필 사진 없음
강좌에 관련 없이 궁금한 내용은 여기를 사용해주세요