개발 이야기/[스터디] c++

[A Tour of C++] Chapter 13. Utilities

경이로운아일라 2024. 8. 26. 19:39
이 글은 <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 &&)
      • 이름이 없는 임시 객체에 대한 참조
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가 클래스일 때만 동작함
}