* C++ noexcept
예외 명세가 "throw()"인 함수에는 그러한 최적화 유연성이 없으며, 예외 명세가 아예 없는 함수 역시 마찬가지로 그런 유연성이 없습니다. 다음은 이상의 여러 상황을 요약한 것입니다.
반환형식 함수이름(매개변수목록) noexcept; // 최적화 여지가 가장 크다
반환형식 함수이름(매개변수목록) throw(); // 최적화 여지가 더 작다
반환형식 함수이름(매개변수목록) // 최적화 여지가 더 작다
예외를 산출하지 않는 함수를 noexcept로 선언하는 것이 좋은 이유는 이 예만으로 충분할 것입니다.
그런데 더욱 강력한 이유가 있는 함수들도 있습니다. 두드러진 예는 이동 연산들입니다. std::vector을 사용하는 어떤 C++98 코드 기반에서, 종종 Widget들을 push_back을 이용해서 벡터에 추가한다고 합시다.
std::vector vw;
...
Widget w;
... // 여기서 w를 사용
vw.push_back(w); // w를 vw에 추가
...
이 코드가 잘 작동한다고 가정합시다. 그리고 이를 C++11에 맞게 수정할 생각은 없지만, C++11의 이동 의미론이 이동 가능 형식이 관여하는 구식 코드의 성능을 자동으로 향상할 수 있다는 점은 활용하고 싶다고 합시다. 그러려면 Widget에 이동 연산들이 갖추어져야 합니다. 그 연산들을 독자가 직접 작성해도 되지만, 요건들이 갖추어진다면(Chapter(17) 컴파일러가 자동으로 작성할 수도 있습니다.
std::vector에 새 요소를 추가할 때, std::vector에 충분한 공간이 없을 수도 있습니다. 이를테면 std::vector의 크기(size)가 용량(capacity)과 같은 상황일 수도 있습니다. 그런 일이 생기면 std::vector는 자신의 요소들을 담을 더 큰 메모리 조각을 새로 할당하고, 기존 메모리 조각의 요소들을 새 조각으로 옮깁니다. C++98에서는 기존 메모리에서 새 메모리로 요소들을 일일이 복사하고 기존 메모리에 있는 객체들을 파괴함으로써 이러한 요소 옮기기를 수행했습니다. 이 접근방식 덕분에 push_back은 강한 예외 안전성을 보장할 수 있었습니다. 즉, 요소들을 복사하는 도중에 예외가 던져져도 std::vector의 상태는 변하지 않습니다. 기존 메모리의 모든 요소가 새 메모리에 성공적으로 복사되기 전에는 기존 메모리의 그 어떤 요소도 파괴되지 않기 때문입니다.
C++11에서는 std::vector 요소들의 복사를 이동으로 대체함으로써 요소 옮기기를 최적화하는 것이 자연스러운 방식입니다. 그러나 그러면 push_back의 예외 안전성 보장이 위반될 수 있습니다. 기존 메모리에서 n개의 요소를 이동한 후 (n+1) 번째 요소를 이동하는 도중에 예외가 발생하면 push_back 연산이 완료되지 못하고 실패합니다. 그런데 원래의 std::vector는 이미 수정된 상태입니다. 요소 중 n개가 다른 곳으로 이동했는데, 그것들을 원래대로 복원하는 것이 불가능할 수 있습니다. 각 객체를 원래의 메모리로 다시 이동하는 연산 자체에서도 예외가 발생할 수 있기 때문입니다.
기존 코드의 행동이 push_back이 보장하는 강한 예외 안전성에 의존할 수도 있다는 점에서, 이는 심각한 문제입니다. 그래서, 이동 연산들이 예외를 방출하지 않음이 확실하지 않은 한 C++11 컴파일러는 push_back 안의 복사 연산들을 소리없이 이동 연산들로 대체하지 않습니다. 이동 연산이 예외를 방출하지 않음이 확실한 경우에는 복사를 이동으로 대체해도 안전합니다. 이 경우 유일한 부수 효과(side effect)는 잠재적인 성능 향상뿐입니다.
std::vector::push_back은, 그리고 표준 라이브러리의 여러 함수는 이러한 "가능하면 이동하되 필요하면 복사한다" 전략을 활용합니다. 특히, C++98에서 강한 예외 안전성을 보장하는 다른 함수들(이르테면 std::vector::reserve, td::deque::insert 등)이 그런 식으로 작동합니다. 이런 함수들은 모두, 오직 이동 연산이 예외를 방출하지 않음이 알려진 경우에만 C++98의 복사 연산을 C++11의 이동 연산으로 대체합니다. 그런데 이동 연산이 예외를 방출하지 않음을 함수가 어떻게 알아낼 수 있을까요?? 답은 명백합니다. 주어진 연산이 noexcept로 선언되어 있는지를 점검하면 됩니다.
(* 대체로 이 점검은 다소 완곡한 방식으로 일어납니다. std::vector::push_back 같은 함수는 std:;move_if_noexcept를 호출하는데, 이것은 형식의 이동 생성자의 noexcept 여부에 따라 오른 값으로의 조건부 캐스팅을 수행하는, std::move의 한 변형입니다(Chapter(23) 참고), 한편 std::move_if_noexcept 자체는 std::is_nothrow_move_constructible을 점검하는데, 이 형식 특질(Chapter(9) 참고)의 값은 컴파일러가 이동 생성자의 noexcept(또는 throw()) 지정 여부를 보고 설정합니다.)
'컴퓨터과학' 카테고리의 다른 글
[Effective C++] (14-4) 예외를 방출하지 않을 함수는 noexcept로 선언할 것 (0) | 2020.06.02 |
---|---|
[Effective C++] (14-3) 예외를 방출하지 않을 함수는 noexcept로 선언할 것 (0) | 2020.06.01 |
[Effective C++] (14-1) 예외를 방출하지 않을 함수는 noexcept로 선언할 것 (0) | 2020.05.31 |
[Effective C++] (13-3) iterator보다 const_iterator를 선호할 것 (0) | 2020.05.31 |
[Effective C++] (13-2) iterator보다 const_iterator를 선호할 것 (0) | 2020.05.31 |