모두의 코드
씹어먹는 C++ - <21. C++ 표준 라이브러리에서 자주 쓰이는 패턴 모음>
안녕하세요 여러분. 이번 강의는 약간의 부록 형식으로 표준 라이브러리에서 자주 쓰이는 패턴들을 간단한 예제 코드들과 함께 정리해 놓은 글입니다. 시간이 날 때 마다 추가해보도록 하겠습니다. 특히 유용한 패턴들을 알고 계신다면 제보 부탁드립니다.
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 차원 벡터에 2000000
을 push_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) | ~~~~~~~~~~~~~^~~
당연하게도 20000
을 std::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++ 20
에 fmt
의 format
함수가 포함되었으니 (아쉽게도 print
는 들어가지 못했습니다.) std::cout << fmt::format("{}", v);
와 같은 식으로 사용하시면 됩니다.
또 참신한 방법으로는 std::copy 와 std::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::transform 과 std::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::map
과 std::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 의 범위만을 바꿔주면 되기 때문이죠.
이를 통해서 lstrip
과 rstrip
을 빠르게 수행할 수 있습니다. 예를 들어서 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()));
와 같이 하면 됩니다.
댓글을 불러오는 중입니다..