귀하의 질문은 "예외 안전 코드 작성이 매우 어렵다"고 주장합니다. 먼저 질문에 답한 다음 숨겨진 질문에 답하겠습니다.
질문에 대답
실제로 예외 안전 코드를 작성합니까?
물론입니다.
이다 자바는 C ++ 프로그래머 (RAII 의미의 부족)으로 나에게 그것의 호소를 많이 잃은 이유, 그러나 나는 탈선 오전 : 이것은 C ++ 질문이다.
실제로 STL 또는 Boost 코드로 작업해야 할 때 필요합니다. 예를 들어, C ++ 스레드 ( boost::thread
또는 std::thread
)는 예외를 throw하여 정상적으로 종료됩니다.
마지막 "제작 준비"코드가 예외 안전하다고 확신하십니까?
확신 할 수 있습니까?
예외 안전 코드 작성은 버그없는 코드 작성과 같습니다.
코드가 예외 안전하다는 것을 100 % 확신 할 수는 없습니다. 그러나 잘 알려진 패턴을 사용하고 잘 알려진 반 패턴을 피하려고 노력합니다.
작동하는 대안을 알고 있거나 실제로 사용하고 있습니까?
C ++ 에는 실행 가능한 대안 이 없습니다 (즉, C로 되돌리고 C ++ 라이브러리와 Windows SEH와 같은 외부 놀라움을 피해야 함).
예외 안전 코드 작성
예외 안전 코드를 작성하려면 먼저 각 명령이 작성하는 예외 안전 수준을 알아야합니다 .
예를 들어, new
는 예외를 던질 수 있지만 내장 (예 : int 또는 포인터)을 할당해도 실패하지 않습니다. 스왑은 절대 실패하지 않습니다 (투척 스왑을 쓰지 마십시오) std::list::push_back
.
예외 보증
가장 먼저 이해해야 할 것은 모든 기능이 제공하는 예외 보증을 평가할 수 있어야한다는 것입니다.
- none : 귀하의 코드는 절대로 그것을 제공해서는 안됩니다. 이 코드는 모든 것을 유출하고 가장 먼저 발생하는 예외를 분석합니다.
- 기본 : 이것은 최소한 제공해야 할 보증입니다. 즉, 예외가 발생하면 리소스가 유출되지 않고 모든 객체가 여전히 전체입니다.
- strong : 처리가 성공하거나 예외가 발생하지만 처리가 시작되면 처리가 전혀 시작되지 않은 것과 동일한 상태가됩니다 (이는 C ++에 트랜잭션 성능을 제공합니다).
- nothrow / nofail : 처리가 성공합니다.
코드 예
다음 코드는 올바른 C ++처럼 보이지만 실제로는 "없음"보증을 제공하므로 올바르지 않습니다.
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
X * x = new X() ; // 2. basic : can throw with new and X constructor
t.list.push_back(x) ; // 3. strong : can throw
x->doSomethingThatCanThrow() ; // 4. basic : can throw
}
이런 종류의 분석을 염두에두고 모든 코드를 작성합니다.
제공되는 최저 보증은 기본이지만 각 명령의 순서는 전체 기능을 "없음"으로 만듭니다. 3. throw가 발생하면 x가 누출되기 때문입니다.
가장 먼저 할 일은 "기본"기능을 만드는 것입니다. 즉, x가 목록에서 안전하게 소유 할 때까지 x를 스마트 포인터에 넣습니다.
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
std::auto_ptr<X> x(new X()) ; // 2. basic : can throw with new and X constructor
X * px = x.get() ; // 2'. nothrow/nofail
t.list.push_back(px) ; // 3. strong : can throw
x.release() ; // 3'. nothrow/nofail
px->doSomethingThatCanThrow() ; // 4. basic : can throw
}
이제 코드는 "기본"보증을 제공합니다. 누수가 없으며 모든 객체가 올바른 상태가됩니다. 그러나 우리는 더 많은 것, 즉 강력한 보증을 제공 할 수 있습니다. 이것은 비용이 많이들 수 있는 곳이므로 모든 C ++ 코드가 강력 하지는 않습니다 . 해 봅시다:
void doSomething(T & t)
{
// we create "x"
std::auto_ptr<X> x(new X()) ; // 1. basic : can throw with new and X constructor
X * px = x.get() ; // 2. nothrow/nofail
px->doSomethingThatCanThrow() ; // 3. basic : can throw
// we copy the original container to avoid changing it
T t2(t) ; // 4. strong : can throw with T copy-constructor
// we put "x" in the copied container
t2.list.push_back(px) ; // 5. strong : can throw
x.release() ; // 6. nothrow/nofail
if(std::numeric_limits<int>::max() > t2.integer) // 7. nothrow/nofail
t2.integer += 1 ; // 7'. nothrow/nofail
// we swap both containers
t.swap(t2) ; // 8. nothrow/nofail
}
우리는 작업을 재정렬하여 먼저 X
올바른 가치를 창출하고 설정 합니다. 조작이 실패하면 t
수정되지 않으므로 조작 1-3 은 "강력한"것으로 간주 될 수 있습니다. 무언가가 발생하면 t
수정 X
되지 않으며 스마트 포인터가 소유하고 있기 때문에 누출되지 않습니다.
그런 다음의 사본 t2
을 작성하고 t
조작 4에서 7까지이 사본에 대해 작업합니다. 무언가가 발생하면 t2
수정되었지만 t
여전히 원본입니다. 우리는 여전히 강력한 보증을 제공합니다.
그런 다음, 우리는 교환 t
및 t2
. 스왑 작업은 C ++에서 무시해야하므로 작성한 스왑 T
이 nothrow (아니면 그렇지 않으면 다시 작성하십시오)가 되기를 바랍니다 .
따라서 함수의 끝에 도달하면 모든 것이 성공하고 (반환 유형이 필요하지 않음) t
예외 값이 있습니다. 실패하면 t
여전히 원래 값을 갖습니다.
이제 강력한 보증을 제공하는 것은 비용이 많이들 수 있으므로 모든 코드에 강력한 보증을 제공하려고 노력하지 마십시오. 그러나 비용없이 할 수 있다면 (그리고 C ++ 인라인 및 기타 최적화로 모든 코드를 비용 효율적으로 만들 수 있습니다) 그런 다음 해보십시오. 기능 사용자가 감사합니다.
결론
예외 안전 코드를 작성하는 습관이 필요합니다. 사용할 각 지침에서 제공하는 보증을 평가 한 다음 지침 목록에서 제공하는 보증을 평가해야합니다.
물론 C ++ 컴파일러는 보증을 백업하지 않지만 (내 코드에서는 @warning doxygen 태그로 보증을 제공합니다) 다소 슬픈 일이지만 예외 안전 코드를 작성하지 못하게해서는 안됩니다.
정상적인 실패와 버그
프로그래머는 실패없는 기능이 항상 성공할 것이라고 어떻게 보장 할 수 있습니까? 결국 함수에 버그가있을 수 있습니다.
사실입니다. 예외 보증은 버그가없는 코드로 제공됩니다. 그러나 모든 언어에서 함수를 호출하면 함수에 버그가 없다고 가정합니다. 제정신의 코드는 버그가있을 가능성으로부터 스스로를 보호하지 않습니다. 코드를 최대한 작성하고 버그가 없다고 가정하여 보증을 제공하십시오. 그리고 버그가 있으면 수정하십시오.
예외는 코드 버그가 아닌 예외적 인 처리 실패에 대한 것입니다.
마지막 말
이제 문제는 "이것이 가치가 있습니까?"입니다.
당연하지. 실패하지 않을 것이라는 것을 알고있는 "Nothrow / no-fail"기능을 갖는 것은 큰 혜택입니다. 커밋 / 롤백 기능이있는 데이터베이스와 같은 트랜잭션 의미론으로 코드를 작성할 수있게하는 "강력한"기능에 대해서도 마찬가지입니다.
그런 다음 "기본"은 제공해야 할 최소한의 보증입니다. C ++은 범위가 매우 강력한 언어이므로 자원 누출을 피할 수 있습니다 (가비지 수집기가 데이터베이스, 연결 또는 파일 핸들에 제공하기 어려운 것).
그래서, 지금까지의 내가 그것을보고, 그것은 이다 가치.
2010-01-29 편집 : 비 투척 스왑 정보
nobar는 "예외 안전 코드를 작성하는 방법"의 일부이기 때문에 관련성이 있다고 생각합니다.
- [me] 스왑은 절대 실패하지 않습니다 (투척 스왑도 쓰지 마십시오)
- [nobar] 사용자 정의 작성
swap()
기능에 대한 권장 사항입니다 . 그러나 std::swap()
내부적으로 사용하는 작업에 따라 실패 할 수 있습니다.
기본값 std::swap
은 일부 객체의 경우 복사 및 할당을 수행하여 복사 할 수 있습니다. 따라서 클래스 또는 STL 클래스에 사용되는 기본 스왑이 발생할 수 있습니다. 지금까지 ++ 표준에 관한 한 C로, 스왑 동작은 vector
, deque
그리고 list
이 수에 대한 반면, 포기하지 않습니다 map
비교 펑은 (참조 복사 건설에 던질 수있는 경우 는 C ++ 언어, 스페셜 에디션, 부록 E, E.4.3 프로그래밍 . 스왑 ).
벡터 스왑의 Visual C ++ 2008 구현을 살펴보면 두 벡터가 동일한 할당 자 (즉, 일반적인 경우)를 가진 경우 벡터 스왑은 발생하지 않지만 할당자가 다른 경우 복사를 수행합니다. 따라서이 마지막 경우에 던질 수 있다고 가정합니다.
따라서 원본 텍스트는 여전히 유지합니다. 던지는 스왑을 작성하지 마십시오. 그러나 nobar의 주석은 기억해야합니다. 스왑하는 객체에 던지지 않는 스왑이 있는지 확인하십시오.
2011-11-06 : 흥미로운 기사 편집
STL 예외를 안전하게 만드는 것에 대한 그의 기사에 기술 된 기본 / 강력 / 무질서 보장 을 제공 한 Dave Abrahams
http://www.boost.org/community/exception_safety.html
7 번째 포인트 (예외 안전을위한 자동화 된 테스트)를 살펴보면 모든 사례를 테스트하기 위해 자동화 된 단위 테스트를 사용합니다. 나는이 부분이 질문 저자의 " 당신도 확신 할 수 있습니까? "에 대한 훌륭한 해답이라고 생각합니다 .
t.integer += 1;
오버플로가 예외적으로 안전하지 않다는 보장이 없으며 실제로 기술적으로 UB를 호출 할 수 있습니다! 부호있는 오버플로는 UB : C ++ 11 5/4 "표현식을 평가하는 동안 결과가 수학적으로 정의되지 않거나 해당 유형의 표현 가능한 값 범위에 있지 않으면 동작이 정의되지 않습니다." 정수는 오버플로되지 않지만 동등 클래스 modulo 2 ^ # bits에서 계산을 수행합니다.
Dionadar는 실제로 다음과 같은 행을 언급하고 있으며 실제로 정의되지 않은 동작이 있습니다.
t.integer += 1 ; // 1. nothrow/nofail
여기서 해결책 std::numeric_limits<T>::max()
은 추가하기 전에 정수가 이미 최대 값에 있는지 확인하는 것입니다 (를 사용하여 ).
내 오류는 "정상적인 오류 대 버그"섹션, 즉 버그입니다. 그것은 추론을 무효화하지 않으며, 예외 안전 코드가 달성 불가능하기 때문에 쓸모가 없다는 것을 의미하지는 않습니다. 컴퓨터 전원을 끄거나 컴파일러 버그, 심지어 버그 또는 기타 오류로부터 자신을 보호 할 수 없습니다. 완벽을 얻을 수는 없지만 가능한 한 가까워 지려고 노력할 수 있습니다.
Dionadar의 의견을 염두에두고 코드를 수정했습니다.