[A Tour of C++] Chapter 13. Utilities
이 글은 <A Tour of C++ Second Edition, Bjarne Stroustrup>를 참고하여 작성하였습니다.
[A Tour of C++] Chapter 13. Utilities
표준 라이브러리에서 제공하는 다양한 유틸리티를 통해 자원 관리, 타입 안전성, 함수 어댑테이션 등 여러 기능을 효율적으로 활용할 수 있습니다.
13.2 Resource Management
표준 라이브러리는 자원 누수가 일어나지 않게 설계되었습니다.
void f() {
scoped_lock<mutex> lck {m}; // mutex 획득
...
// scoped_lock 소멸자가 불릴 때, mutex도 해제
}
스마트 포인터: unique_ptr과 shared_ptr
포인터를 사용해야 할 때는 스마트 포인터를 사용하는 것이 좋습니다.
- 스마트 포인터를 사용하면 해당 자원이 더이상 사용되지 않을 때 알아서 자원이 해제됨을 보장받을 수 있습니다.
표준 라이브러리는 생성과 초기화를 동시에 할 수 있도록 make_shared, make_unique 제공합니다.
auto p1 = make_shared<S>(1, "Anhn", 4.65)
auto p1 = make_unique<S>(1, "Oz", 4.65)
스마트 포인터 종류
- unique_ptr: 소유자가 하나뿐인 객체의 포인터가 필요할 때 사용하는 스마트 포인터
- return 시: 이동 연산자 호출
- shared_ptr: 둘 이상에게 공유된 객체에 대한 포인터가 필요할 때 사용하는 스마트 포인터
- return 시: 복사되며 이때 객체에 대한 참조카운트가 증가
- 참조카운트가 0이되면 해당 객체 해제
move()와 forward()
- 참조의 두 가지 방식
- l value reference(T &)
- 메모리 상에 명명된 객체, 즉 주소를 가진 객체에 대한 참조
- r value reference(T &&)
- 이름이 없는 임시 객체에 대한 참조
- l value reference(T &)
void foo(int& a) {
cout << "lvalue reference" << endl;
}
void foo(int&& a) {
cout << "rvalue reference" << endl;
}
int main() {
int a = 10;
foo(a); // lvalue reference
foo(10); // rvalue reference
}
- 크기가 클지도 모를 객체를 계속해서 복사하고 싶지 않다면 std::move를 사용하는 것이 좋습니다.
string s1 = "Hello";
string s2 = "World";
vector<string> v;
v.push_back(s1); // 복사
v.push_back(move(s2)); // 이동
cout << s1; // OK
cout << s2; // ERROR: 이동 후에는 원본 객체 접근 불가
- 인자의 집합을 아무 변경 없이 다른 함수로 전달하고 싶을 때, std::forward를 사용해야 합니다.
void doSomething(CustomVector& vec) {
cout << "Pass by L-reference" << endl;
}
void doSomething(CustomVector&& vec) {
cout << "Pass by R-reference" << endl;
}
template<typename T>
void doSomethingTemplate(T && vec) {
//doSomething(vec); // l-value 참도로 전달됨
doSomething(std::forward<T>(vec));
// vec 이제 접근 불가
}
13.3 Range Checking: gsl::span
연속된 데이터 구조의 배열을 넘길 때, 구간 검사도 강제하고 싶다면 span을 사용하는 것이 좋습니다.
void fpn(int *p, int n) {
for (int i = 0; i < n; ++i)
p[i] = 0;
}
void fs(span<int> p) {
for (int x: p)
x = 0;
}
void main() {
int a[100];
fpn(a+10, 100); // iteration 돌다가 에러
fs({a+10, 100}); // span 만들다가 에러
}
13.4 Specialized Container
하나의 컨테이너만으로 모든 프로그래머의 요구를 만족시킬 수 없습니다. 아래처럼 다양한 컨테이너가 준비되어 있으니, 본인의 상황에 적합한 컨테이너를 사용하는 것이 좋습니다.
- 고정 크기(T[N], array, bitset, …) vs 크기 늘리는 기능(vector, map, …)
- 연속된 위치에 할당(array, vector, tuple, …) vs 연결되도록 할당(forward_list, map, …)
array<T, N>
array<T, N>은 T 타입의 요소 N개가 연속적으로 할당된 고정크기 시퀀스입니다.
언제 vector 대신 array인가
- vector는 free space에 저장, array는 stack에 저장
- stack 메모리는 free space로, 힙에 비해 빠르게 접근할 수 있다
왜 내장배열보다 array인가
- array의 크기를 알 수 있다
- =을 이용해서 복사할 수 있다
- 예기치 않은 포인터로의 변환을 예방할 수 있다
// sizeof(Shape) < sizeof(Circle)
void h() {
Circle a1[10];
array<Circle, 10> a2;
Shape * p1 = a1; // OK: 재앙의 시작
Shape* p2 = a2; // 에러: arrays<Circle,10>를 Shape*으로 변환할 수 없음
p1[3].draw); // 재앙 발생: 오프셋이 틀리다
}
bitset<N>
N비트의 고정 크기 시퀀스로 비트 연산도 제공하는 유틸입니다.
bitset<9> bs1 {"110001111"};
bitset<9> bs3 = ^bs1; // 보수: 001110000
pair와 tuple
- 값의 모음이 필요할 때 사용하는 유틸입니다.
- 2개라면 pair, 그 이상은 tuple을 사용할 수 있다
- 꼭 필요한 게 아니라면 struct가 유지 보수성 면에서 더 나을 수 있다
- 각 데이터가 어떤 의미인지 조금 더 확실하게 다룰 수 있음
13.5 Alternatives
variant
지정된 type 중 하나를 표현하는 유틸입니다.
void check(variant<Expression,Statement,Declaration,Type>∗ p) {
if (holds_alternative<Expression>(∗p)) {
Expression& e = get<Expression>(∗p);
// ...
}
else if (holds_alternative<Statement>(∗p)) {
Statement& s = get<Statement>(∗p);
// ...
}
// ... Declaration and Type ...
}
위와 같은 코드를 쓰고 싶을 때, 다른 방법으로는 아래와 같이 작성할 수 있습니다.
void check(Node∗ p) {
visit(overloaded {
[](Expression& e) { /* ... */ },
[](Statement& s) { /* ... */ },
// ... Declaration and Type ...
}, ∗p);
}
optional
- 지정된 타입의 값이나 값이 없음(nullptr)을 표현하는 타입입니다.
- 저장된 값이 없는 optional에 접근할 경우 어떤 일이 벌어질 지는 예측할 수 없고 예외도 던지지 않습니다. 따라서, optional은 타입안전성을 보장한다고 볼 수 없습니다.
any
임의의 타입을 저장할 수 있는 타입입니다.
13.6 Allocators
pool allocator
pool allocator를 이용해 객체를 할당하면,
- 공통된 단일 크기의 객체를 매번 할당하기 보다는
- 많은 객체를 한번에 할당해 Fragmentation 으로 인한 메모리 낭비를 막을 수 있습니다.
std::vector<int, boost::pool_allocator<int>> v;
13.7 Time
표준 라이브러리 <chrono> 에서는 아래와 같은 시간을 다루는 기능을 제공하고 있습니다.
- time_point 간의 duration 계산
- 시간 단위 변환 duration cast도 제공(e.g. seconds ↔ milliseconds)
- 시간 단위의 접미사 제공
13.8 Function Adaptation
함수를 다른 함수의 인자로 전달할 때에는 인자의 타입이 호출될 함수의 타입과 정확히 일치해야 한다. 그렇지 않을땐?
방법1. 람다를 어댑터로 사용
void draw_all(vector<Shape*>& v) {
for_each(v.begin(), v.end(),
[](Shape* p){p->draw();}
)
}
방법2. std::mem_fn 으로 만든 함수 객체 활용
void draw_all(vector<Shape*>& v) {
for_each(v.begin(), v.end(),
mem_fn(&Shape::draw)
)
}
방법3. std::function으로 만든 함수 객체 활용
void draw_all(vector<Shape*>& v) {
function fct3 = [](Shape* p) I p->draw();
for_each(v.begin(), v.end(),
fct3,
)
}
13.9 Type Function
컴파일 시간 공안 더 엄격한 타입 검사와 성능 향상을 가져오는 방식을 메타프로그래밍 혹은 템플릿 메타프로그래밍이라고 합니다.
iterator_traits
iterator_traits는 반복자에 대한 타입 정보를 제공합니다.
- value_type: 반복자가 가리키는 요소의 타입.
- iterator_category: 반복자의 카테고리(예: 순방향 반복자, 임의 접근 반복자).
- difference_type: 반복자 간의 거리 타입.
- pointer: 반복자가 가리키는 요소의 포인터 타입.
- reference: 반복자가 가리키는 요소의 참조 타입.
tag dispatch
빈 구조체 태그를 이용해서 어떤 함수를 부를 것인지 고르는 방법을 말합니다.
struct tag1{};
struct tag2{};
auto f(int a, tag1 dummy){
std::cout<< a << "via tag1 \n";
}
auto f(int a, tag2 dummy){
std::cout<< a << "via tag2 \n";
}
type predicates
템플릿 작성할 때, is_arithmetic, is_literal_type 등과 같은 type predicates를 이용해 타입 검사를 하기도 합니다.
template<typename Scalar>
class complex {
// ...
public:
static_assert(Is_arithmetic<Scalar>(), "타입 에러")
}
enable_if
조건에 따라 정의를 선택적으로 활성화할 때 사용합니다.
template<typename T>
class Smart_pointer {
T& operator*();
std::enable_if<Is_class<T(), T&>> operator->(); // T가 클래스일 때만 동작함
}