References
- The C++ Standard Library: Second Edition includes C++17
- https://en.cppreference.com/w/
Contents
- C++ History
- C++ 라이브러리 개요
C++ History
C++과 표준 라이브러리의 역사는 오래되었습니다. C++은 1980년대에 등장해서 2017년 버전, 현재는 2020년 버전까지 이어져오고 있습니다. 지난 30~40년간 C++이 얼마나 달라졌는지는 C++ 표준 라이브러리만 봐도 알 수 있는데, C++은 객체지향 언어(object-oriented language)로 시작했다가, STL을 이용한 제너릭 프로그래밍 개념이 추가되고 최근에는 함수형 프로그래밍 개념도 상당히 반영되고 있습니다.
C++의 발전 과정을 간단하게 요약하면 다음과 같이 나타낼 수 있습니다.
1998년에 등장한 최초의 C++ 표준 라이브러리는 3가지 컴포넌트로 구성됩니다. 주로 파일 처리에 사용되는 I/O 스트림, String 라이브러리, 표준 템플릿 라이브러리(STL; Standard Template Library)입니다.
2005년에 등장한 TR1(Technical Report 1)은 C++ 라이브러리 확장 기능(ISO/IEC TR 19768)에 대한 문서로서 공식 표준은 아니지만, 여기에 등장한 거의 모든 컴포넌트가 C++11에서 포함되었습니다. 예를 들어, 정규표현식(Regular expressions), 스마트 포인터(Smart pointers), 해시 테이블(Hash tables), 난수(Random numbers), 시간(Time) 등에 대한 라이브러리가 정의되었는데, 이는 부스트(Boost) 라이브러리(https://www.boost.org/)에 토대를 두고 있습니다.
C++11부터 TR1의 표준화와 함께 멀티스레딩 라이브러리가 추가되었습니다.
C++14는 C++11 표준을 약간 업데이트한 수준에 불과합니다. 스마트 포인터, 튜플, type traits, 멀티스레딩에 대한 몇 가지 개선 사항만 추가되었습니다.
C++17부터는 파일 시스템 라이브러리, std::any, std::optional이란 데이터 타입이 새롭게 추가되었습니다. C++20부터는 네트워크 프로그래밍 라이브러리와 템플릿 파라미터에 대한 타입 시스템이 추가되고 멀티스레딩에 대한 지원이 향상되었습니다.
C++ 라이브러리 개요
C++에서 제공하는 라이브러리가 너무 방대하여 사실 모든 라이브러리를 살펴보기가 힘듦니다. 이번 포스팅에서는 표준 라이브러리가 크게 어떻게 구성되는지 알아보고, 추후에 자주 사용되는 라이브러리를 중심으로 살펴보도록 하겠습니다.
유틸리티 Utility
https://en.cppreference.com/w/cpp/utility#General-purpose_utilities
유틸리티(utility) 라이브러리는 여러 상황에서 사용할 수 있는 범용 기능들을 모아둔 라이브러리입니다. 예를 들면, 최솟값, 최댓값을 구하거나 값을 swap 또는 move하는 함수가 있습니다.
또 다른 예로 std::function과 std::bind가 있는데, std::bind를 사용하면 기존 함수를 토대로 새로운 함수를 쉽게 만들 수 있습니다. 이렇게 만든 함수를 나중에 호출하기 위해서 변수에 바인딩하려면, std::function을 사용합니다.
std::pair나 이를 일반화한 std::tuple도 포함하고 있습니다.
레퍼런스 래퍼(reference wrapper)인 std::ref와 std::cref도 꽤 유용합니다. std::ref는 변수에 대한 레퍼런스 래퍼 오브젝트를 생성하고 const 레퍼런스는 std::cref로 생성합니다.
유틸리티 라이브러리의 핵심은 C++에서 메모리를 명시적으로 자동 관리해주는 스마트 포인터(smart pointer)입니다. 단독 소유권은 std::unique_ptr로, 공유되는 포인터는 std::shared_ptr로 표현합니다. std::shared_ptr은 해당 포인터를 참조하는 레퍼런스의 수를 세는 방식(reference counting)으로 리소스를 관리합니다.
std::weak는 레퍼런스 카운팅의 고질적인 문제인 순환 참조(circular reference)를 해결하는데 도움이 됩니다.
Type Traits 라이브러리(<type_traits>)는 컴파일 시간에 타입 정보를 검사, 비교, 조작하는데 사용됩니다.
Time 라이브러리(<chrono>)는 C++에서 성능을 측정하는데 유용하게 사용됩니다.
C++17에서 추가된 특수 데이터 타입인 std::any, std::optional, std::variant도 있는데, 자세한 것은 추후에 알아보도록 하겠습니다.
표준 템플릿 라이브러리(STL; The Standard Template Library)
STL은 크게 3가지 요소로 구성됩니다.
이 요소는 컨테이너(Containers), 컨테이너에서 실행되는 알고리즘(Algorithms), 그리고 컨테이너와 알고리즘을 연결해주는 반복자(Iterators)입니다. 이렇게 추상화된 제너릭 프로그래밍(generic programming)을 통해 알고리즘과 컨테이너를 고유한 방식으로 결합할 수 있습니다. 단지 컨테이너의 원소에 대해서 최소한의 요구사항만을 지키면 됩니다.
C++ 표준 라이브러리는 다양한 컨테이너를 제공합니다. 크게 순차(sequential) 컨테이너와 연관(associative) 컨테이너로 구분할 수 있습니다. 연관 컨테이너는 순서가 있는 컨테이너(ordered associative container)와 순서가 없는 컨테이너(unordered associative container)로 나눌 수 있습니다.
순차 컨테이너에 해당하는 컨테이너마다 고유한 용도가 있지만, 대부분은 std::vector로 충분합니다. std::vector는 크기를 동적으로 조절할 수 있고, 메모리를 자동으로 관리해주며 성능도 좋습니다. 이와 대조적으로 std::array는 순차 컨테이너 중에서 유일하게 크기를 런타임에 조절할 수 없습니다. std::array는 메모리 사용량을 최소화하도록 성능을 최적화한 것입니다.
std::vector는 새로운 원소를 맨 뒤에 추가하지만, std::deque는 원소를 맨 앞에서 추가할 수도 있습니다.
std::list는 이중 연결 리스트(doubly linked list), std::forward_list는 단일 연결 리스트(singly linked list)입니다. 둘 다 컨테이너에서 임의의 지점에 매우 빠르게 접근할 수 있도록 최적화되었습니다.
연관 컨테이너는 키-값(key-value) 쌍으로 구성된 컨테이너로, 주어진 키에 대한 값을 제공하는 형태로 구성됩니다. C++에서는 8가지의 연관 컨테이너를 제공하며, 순서가 있는 연관 컨테이너와 순서가 없는 연관 컨테이너로 분류합니다.
순서가 있는 컨테이너는 키를 기준으로 정렬하며 std::set, std::map, std::multiset, std::multimap이 있습니다.
순서가 없는 컨테이너에는 std::unordered_set, std::unordered_map, std::unordered_multiset, std::unordered_multimap이 있습니다.
먼저 순서가 있는 연관 컨테이너부터 간단하게 살펴보면, std::set은 키에 연관된 값이 없고 std::map은 키에 연관된 값이 있습니다. 그리고 std::map은 키가 유일하지만, std::multimap은 키가 중복될 수 있습니다. 이러한 속성들은 순서가 없는 연관 컨테이너에도 동일하게 적용되며, 유사한 점이 많습니다.
순서가 있는 연관 컨테이너와 순서가 없는 연관 컨테이너의 가장 큰 차이점은 성능입니다. 순서가 있는 연관 컨테이너에서 액세스 시간은 원소의 개수에 대해 로그 함수로 증가하지만, 순서가 없는 연관 컨테이너는 액세스 시간이 일정합니다. 즉, 순서가 없는 연관 컨테이너의 액세스 시간은 크기와 관계가 없습니다. 연관 컨테이너를 사용하는 경우의 대부분은 키를 기준으로 정렬하는 std::map이 적합합니다.
컨테이너 어댑터(container adapter)는 순차 컨테이너에 대한 간편한 인터페이스를 제공합니다. C++에서는 위에서 보여주는 std::stack, std::queue, std::priority_queue를 제공합니다.
반복자(Iterators)는 컨테이너와 알고리즘을 연결해줍니다. 반복자는 컨테이너에 의해서 생성되며 범용 포인터로 구성됩니다. 반복자는 컨테이너에 대한 반복문을 임의의 위치를 기준으로 forward/backward로 진행하는데 사용됩니다. 반복자의 종류는 컨테이너마다 다르며, 반복자 어댑터(iterator adapter)를 사용하면 스트림(stream)에 직접 액세스할 수 있습니다.
STL에는 100개가 넘는 알고리즘(algorithms)을 제공합니다.
execution policy에 따라 거의 모든 알고리즘을 실행할 수 있습니다. 알고리즘은 원소 또는 원소 범위(range)에 대해 동작하며, 범위는 시작 지점을 가리키는 시작 반복자(begin iterator)와 끝 지점을 지정하는 끝 반복자(end iterator)로 지정합니다. 참고로 end iterator는 범위의 끝에 해당하는 원소가 아니라 그 다음 지점을 가리킨다는 점에 주의해야합니다.
알고리즘의 활용 범위는 매우 넓습니다. 원소를 검색하거나 개수를 카운트할 수 있으며, 범위를 찾거나 원소끼리 비교하고 swap할 수도 있습니다. 컨테이너에서 원소를 생성하거나 교체하거나 제거하는 알고리즘도 있습니다. 물론 정렬(sort), 순열(permute), 분할(partition)하는 알고리즘도 있고, 컨테이너에서 최대 원소와 최소 원소를 구하는 알고리즘도 있습니다. 또한, 콜러블(callable)이나 함수 오브젝트(function objects), 람다 함수(lambda function) 등으로 커스터마이즈할 수도 있습니다.
수치 연산 Numeric
C++에서 제공하는 수치 연산 관련 라이브러리는 두 가지가 있는데, 하나는 난수 라이브러리(<random>)이고 다른 하나는 C로부터 상속받은 수학 함수(<cmath>)입니다.
난수 라이브러리는 두 부분으로 구성되는데, 하나는 난수 생성기(random number generator)이고 다른 하나는 생성된 난수의 분포에 대한 것입니다. 난수 생성기는 최댓값과 최솟값 사이에 있는 일련의 난수를 생성하며, 이렇게 생성되는 난수는 특정한 분포를 따릅니다.
C++에는 C로부터 수많은 표준 수학 함수(mathematical standard function)을 물려받았는데, 예를 들어, 로그 함수/지수 함수/삼각 함수 등이 있습니다.
텍스트 처리 Text Processing
C++에서는 텍스트 처리를 위한 String과 정규 표현식(regex expressions)에 관련된 강력한 두 가지 라이브러리를 제공합니다.
std::string은 텍스트를 분석하고 수정에 관련된 다양한 메소드를 제공합니다. char 타입의 원소를 갖는 std::vector와 공통점이 많기 때문에 STL 알고리즘을 std::string에도 사용할 수 있습니다. std::string은 C의 스트링을 조금 더 쉽고 안전하게 사용하도록 확장한 것이며, 메모리를 직접 관리합니다.
std::string과 달리 std::string_view는 복사 연산이 가볍습니다. std::string_view는 std::string에 대한 non-owning reference(read-only) 입니다.
정규 표현식은 텍스트 패턴을 표현하는 언어이며, 주어진 텍스트에 원하는 패턴이 있는지 찾는 용도로 사용합니다. 또한, 패턴과 일치하는 부분을 다른 텍스트로 대체하는 용도로도 많이 사용됩니다.
입력과 출력 Input and Output
I/O 스트림 라이브러리(I/O stream library)는 C++ 초기부터 제공된 라이브러리로, 외부와 소통(communication)하는 기능을 제공합니다.
여기서 커뮤니케이션의 의미를 구체적으로 설명하면, 추출 연산자(extraction operator) '>>'은 입력 스트림으로부터 서식이 있거나 없는 데이터를 읽고, 삽입 연산자(insertion operator) '<<'는 출력 스트림에 데이터를 씁니다. 데이터의 서식(format)은 조작자(manipulator)로 지정합니다.
스트림 클래스는 클래스 계층 구조가 정교합니다. 그중에서 두 가지 클래스가 중요한데, 하나는 스트링 스트림(string stream)이며 string과 stream 간의 상호 작용하도록 할 수 있습니다. 다른 하나는 파일 스트림(file stream)이며, 파일을 쉽게 읽고 쓸 수 있도록 해줍니다. 스트림의 상태는 flag로 기록되며 사용자가 읽거나 설정할 수 있습니다.
삽입 연산자(insertion operator)와 추출 연산자(extraction operator)를 오버로딩(overloading)하면 기본 데이터 타입을 사용하는 방식처럼 외부와 상호 작용할 수 있습니다.
I/O 스트림 라이브러리와 달리 파일 시스템 라이브러리(filesystem library)는 C++17부터 표준에 추가되었습니다. 이 라이브러리는 세 가지 컨셉인 file, file name, path를 토대로 구성됩니다. file에는 디렉토리, hard link, symbolic link 또는 regular file(일반 파일)이 있으며, path에는 절대 경로와 상대 경로가 있습니다.
파일 시스템 라이브러리는 파일 시스템을 읽거나 조작하기 위한 강력한 인터페이스들을 제공합니다.
멀티스레딩 Multithreading
2011년에 발표된 C++11부터 멀티스레딩 라이브러리(<thread>)가 표준으로 들어왔습니다. 이 라이브러리는 atomic 변수, 스레드(thread), lock과 condition variable과 같은 기본 요소들로 구성됩니다. 이를 토대로 고급화된 추상화가 제공되는데, C++11부터 이미 task라는 고급 추상화를 제공합니다.
스레드에 관련된 내용은 추후에 다룰 예정이긴 하지만, 이전 포스팅에서 다룬적이 있으니 이를 참조하셔도 좋을 것 같습니다.
'프로그래밍 > C & C++' 카테고리의 다른 글
[C++] 메모리 관리 (1) - 동적 메모리, 배열과 포인터 (0) | 2022.02.07 |
---|---|
[C++] string과 string_view (0) | 2022.02.06 |
[C++] 비동기(Asynchronous) 실행 (0) | 2021.08.14 |
[C++] Perfect Forwarding (0) | 2021.08.13 |
[C++] Move Semantics (0) | 2021.08.12 |
댓글