19. Vector, Templates, and Exceptions

19.1. The problems

std::vector의 설계에 대한 다음 고려 사항들은 중요하다.

  • 크기를 어떻게 변경하나?
  • 범위 밖 접근을 어떻게 감지하고 처리하나?
  • 항목 타입을 어떻게 지정하나?

19.2. Changing size

std::vector는 단순하게는 3가지 방법으로 크기를 바꿀 수 있다.

  • resize().
  • push_back().
  • 다른 std::vector를 대입.

19.2.1. Representation

std::vector의 구현은 원소의 수, 첫 번째 원소의 주소와 함께 여유 공간을 제공한다. 이는 재할당이 매번 일어나지 않게 하기 위함이다. 기본 생성자는 여유 공간을 두지 않는다.

19.2.2. reserve and capacity

reserve()는 여유 공간을 잡고, capacity()는 여유 공간 (원소들이 차지하고 있는 크기가 아니다) 을 리턴한다. 즉, capacity() – size() 만큼의 원소를 push_back()으로 재할당 없이 추가할 수 있다.

19.2.3. resize

resize는 원소들이 차지하고 있는 크기를 변경시킨다.

19.2.4. push_back

push_back은 구현에 따라 다르지만 대개 할당된 공간이 꽉 차면 해당 공간을 2배로 재할당한다. 이는 분할 상환 선형 시간을 가능케 한다.

19.2.5. Assignment

대입 연산자는 대상 vector만큼의 크기를 재할당한 뒤 대상 원소들을 복제한다. 크기가 같거나 대상이 같을 경우 복제를 하지 않는 등의 최적화도 가능하다.

19.2.6. Our vector so far

이제 기본적인 구현은 되었다.

19.3. Templates

템플릿은 클래스나 함수에 대해 타입을 인자로 넣을 수 있게 해 주는 기능이다. 이후에 프로그래머가 특정한 타입을 인자로 넣으면 컴파일러가 해당 타입에 맞는 클래스나 함수를 생성해 준다.

19.3.1. Types as template parameters

템플릿 타입 문법은 template <typename T> 또는 template <class T>로 쓴다. 컴파일러에 의한 템플릿 인스턴스화는 컴파일 타임에 이루어진다.

19.3.2. Generic programming

C++의 제네릭 프로그래밍은 템플릿을 통해 이루어진다. 제네릭 프로그래밍은 적합한 여러 타입들에 대해 그 타입이 인자로 주어졌을 때 동작하는 코드를 쓰는 것이다. 이를 통해서 정적 다형성 또는 매개화 다형성, 컴파일 타임 다형성을 얻을 수 있다. 이는 객체지향 프로그래밍 내지는 즉각적 다형성, 런타임 다형성과는 다른 것이다. 두 패러다임을 혼합해 사용하는 것도 가능하다.

19.3.3. Concepts

C++20부터는 템플릿 인자에 대한 조건을 컨셉으로 걸 수 있다. 이는 C++17까지의 템플릿의 단점인, 구현과 인터페이스가 깔끔하게 분리되지 않는다는 점을 완화시킨다.

19.3.4. Containers and inheritance

클래스 D가 클래스 B의 파생 클래스일 때 D로 인스턴스화된 템플릿 클래스를 B 타입의 해당 템플릿 클래스에 대입할 수 없다. 이는 슬라이싱을 막기 위한 것이다.

19.3.5. Integers as template parameters

template <typename T, int N> 처럼 정수 타입을 템플릿 인자로 넘기는 것도 가능하다. C++20부터는 부동 소수점도 인자로 넘길 수 있다. std::array 같은 경우 이를 사용한다. 이는 std::vector에 비해 컴파일 타임에 모든 것이 결정된다는 이점을 갖는다. 또한, C 스타일 배열이 가진 여러 문제 (사이즈를 알 수 없음, 포인터로 암묵적 축소됨) 도 없다.

19.3.6. Template argument deduction

템플릿 함수에 대해서는, 컴파일러는 가능한 경우 함수 인자로부터 템플릿 인자를 유추한다.

19.3.7. Generalizing vector

std::vector는 기본 생성자가 없는 타입에 의해 매개화되었을 때 resize 등으로 기본값을 사용할 수 없다.

std::vector는 항목들을 소멸시킬 때 메모리 할당자에 의존한다.

19.4. Range checking and exceptions

std::vector의 항목 접근에 대해 범위 체크를 하고 싶다면 .at()을 쓴다. 이는 부당한 접근에 대해 std::out_of_range 예외를 던진다.

19.4.1. An aside: design considerations

그러면 왜 범위 체크를 operator[]에 넣지 않았는가? 이는 예외가 허용되지 않는 환경과 범위 체크를 필요할 때만 쓰겠다는 효율성 때문이다.

19.4.1.1. Compatibility

이미 존재하는 코드와 호환성을 유지하는 것은 중요하다. C++은 그렇게 발전되어 왔다.

19.4.1.2. Efficiency

범위 체크를 강제하지 않는 것은 효율성을 가져다주지만, 효율성이 굳이 절실하지 않다면 범위 체크를 하자.

19.4.1.3. Constraints

효율성이 굳이 절실하지 않다면, 예외에 기반한 오류 처리와 범위 체크를 하자.

19.4.1.4. Optional checking

C++ 표준에서는 범위 체크를 강제하지 않지만, 하는 것이 좋다.

19.4.2. A confession : macros

이 책에서는 Vector가 std::vector를 상속하게 한 뒤 범위 체크 기능을 넣고 #define vector Vector라는 매크로 트릭으로 std::vector를 대체하였다. 웬만하면 이런 일을 하지 말자. 매크로는 C++에서 버그의 온상이다.

19.5. Resources and exceptions

예외는 적절하게 처리하라고 던지는 것이다. try 블록으로 예외를 받은 뒤 에러 메시지를 띄우고 프로그램을 종료하는 것으로는 충분치 않다.

std::vector에서는 획득한 자원을 해제함을 반드시 보장한다.

19.5.1. Potential resource management problems

new-delete를 생성자/소멸자 밖에서 쓰지 말라. 자원 관리하기도 힘들고 유지보수하기 힘들어진다.

19.5.2. Resource acquisition is initialization

생성자에서 자원을 획득하고 소멸자에서 자원을 해제한다. 이것이 RAII로 불리는 C++의 기본 테크닉이다.

19.5.3. Guarantees

try-catch 블록을 쓰는 정석적인 법은 catch 블록에서 지역적으로 획득한 자원들을 전부 해제한 뒤 호출자에 다시 예외를 throw하는 것이다. 또한, 함수가 예외를 던졌을 때 입력값이 바뀌면 안 된다. 예외를 던지지 않음을 보장하려면 noexcept를 붙여 주자. 그러나 가장 기본은 불합리한 연산을 가급적 하지 않는 것이다.

19.5.4. unique_ptr

std::unique_ptr은 RAII가 적용되는 포인터이다. 가리키는 오브젝트에 대한 메모리 소유권을 가지며, 때문에 이동 연산만 가능하고 대입 연산이 불가능하다. 대입 연산을 하려면 std::shared_ptr을 쓴다. 이는 오버헤드가 존재한다.

19.5.5. Return by moving

이동 생성자와 이동 대입 연산자를 구현해서 이동에 의한 반환을 일어나게 하라.

19.5.6. RAII for vector

std::vector는 std::swap을 내부 구현에 써서 RAII 구현을 용이하게 한다.

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중