C ++에 '최종'구성이없는 이유는 무엇입니까?


57

C ++에서의 예외 처리는 try / throw / catch로 제한됩니다. Object Pascal, Java, C # 및 Python과 달리 C ++ 11에서도 finally구문이 구현되지 않았습니다.

"예외 안전 코드"에 대해 언급 한 많은 C ++ 문헌을 보았습니다. Lippman은 예외 안전 코드가 Primer의 범위를 넘어서는 중요하지만 진보 된 어려운 주제라고 썼다. 이는 안전한 코드가 C ++의 기본이 아니라는 것을 암시하는 것으로 보인다. Herb Sutter는 그의 뛰어난 C ++에서 10 개의 챕터를 주제에 전념합니다!

그러나 "예외 안전 코드"를 작성하려고 할 때 발생하는 많은 문제는 finally구문이 구현 되면 상당히 잘 해결 되어 프로그래머가 예외가 발생하더라도 프로그램을 복원 할 수 있도록 보장합니다 안전하고 안정적이며 누출이없는 상태, 리소스 할당 지점 및 잠재적으로 문제가있는 코드에 가깝습니다. 매우 숙련 된 Delphi 및 C # 프로그래머로서 try.를 사용합니다.이 언어로 된 대부분의 프로그래머와 마찬가지로 마침내 코드에서 광범위하게 차단됩니다.

C ++ 11에서 구현 된 모든 '벨과 휘파람'을 고려할 때 '마지막'이 아직 존재하지 않는다는 사실에 놀랐습니다.

그렇다면 왜 finally구조가 C ++로 구현되지 않았습니까? 프로그래머가 '예외 안전 코드'를 작성하도록 돕는 것은 그리 어렵거나 진보 된 개념이 아니며 먼 길을 간다.


25
왜 안돼? 객체 (또는 스마트 포인터)가 범위를 벗어날 때 자동으로 발생하는 소멸자에서 항목을 해제하기 때문입니다. 소멸자는 워크 플로를 정리 논리와 분리하므로 최종적으로 {}보다 우수합니다. free ()에 대한 호출을 원하지 않는 것처럼 가비지 수집 언어로 워크 플로를 복잡하게 만듭니다.
mike30


8
" finallyC ++ 에는 왜 없는가 ? 그리고 그 대신에 어떤 예외 처리 기술이 사용됩니까?" 이 사이트에 대한 유효한 주제입니다. 기존 답변이 이것을 잘 다루고 있다고 생각합니다. "C ++ 디자이너의 finally가치 가없는 이유는 무엇입니까?" 에 대한 토론으로 전환 " finallyC ++에 추가 해야합니까 ?" 질문에 대한 의견을 통해 토론을 진행하며 모든 답변이이 Q & A 사이트의 모델에 맞지 않습니다.
Josh Kelley

2
마지막으로 문제가 있다면 이미 분리되어 있습니다. 기본 코드 블록이 여기 있으며 정리 문제가 여기에서 처리됩니다.
Kaz

2
@ 카즈. 차이점은 암시 적 대 명시 적 정리입니다. 소멸자는 스택에서 튀어 나올 때 일반 오래된 기본 요소를 정리하는 방법과 유사한 자동 정리를 제공합니다. 명시적인 정리 호출을 할 필요가 없으며 핵심 논리에 집중할 수 있습니다. 시도 / 마지막으로 스택 할당 프리미티브를 정리해야한다면 얼마나 복잡한 지 상상해보십시오. 암시 적 정리가 우수합니다. 클래스 구문과 익명 함수의 비교는 관련이 없습니다. 핸들을 해제하는 함수에 일급 함수를 전달하면 수동 정리를 중앙 집중화 할 수 있습니다.
mike30

답변:


56

@ Nemanja의 답변에 대한 추가 논평으로 (Stroustrup을 인용하기 때문에 실제로 얻을 수있는만큼 좋은 답변입니다) :

그것은 실제로 C ++의 철학과 관용구를 이해하는 문제입니다. 영속 클래스에서 데이터베이스 연결을 여는 조작의 예를 들어 예외가 발생하면 해당 연결을 닫아야합니다. 이것은 예외 안전 문제이며 예외가있는 모든 언어 (C ++, C #, Delphi ...)에 적용됩니다.

try/ 를 사용하는 언어 finally에서 코드는 다음과 같습니다.

database.Open();
try {
    database.DoRiskyOperation();
} finally {
    database.Close();
}

간단하고 간단합니다. 그러나 몇 가지 단점이 있습니다.

  • 언어에 결정 론적 소멸자가 없으면 항상finally 블록 을 작성해야 합니다. 그렇지 않으면 리소스가 누출됩니다.
  • 경우는 DoRiskyOperation하나의 메소드 호출보다 더 - 나는에서 할 약간의 가공이있는 경우 try차단 - 다음 Close작업이 괜찮은 조금 멀리에서 끝나게 수 있습니다 Open작업. 획득 한 바로 옆에 정리를 쓸 수 없습니다.
  • 획득해야 할 리소스가 여러 개인 경우 예외 안전 방식으로 해제하면 여러 계층의 깊이 try/ finally블록으로 끝날 수 있습니다 .

C ++ 접근 방식은 다음과 같습니다.

ScopedDatabaseConnection scoped_connection(database);
database.DoRiskyOperation();

이것은 finally접근법 의 모든 단점을 완전히 해결합니다 . 자체 단점이 있지만 상대적으로 작습니다.

  • ScopedDatabaseConnection수업을 직접 작성해야 할 좋은 기회가 있습니다 . 그러나 4 ~ 5 줄의 코드만으로 매우 간단한 구현입니다.
  • 여기에는 여분의 로컬 변수를 만드는 것이 포함됩니다. "파괴자를 정리하기 위해 소멸자에 의존하는 클래스를 지속적으로 만들고 파괴하는 것에 대한 의견을 바탕으로 팬이 아닙니다." 여분의 지역 변수와 관련된 추가 작업 중 하나. 좋은 C ++ 디자인은 이러한 종류의 최적화에 많이 의존합니다.

개인적으로 이러한 장단점을 고려할 때 RAII가보다 바람직한 기술이라고 생각합니다 finally. 귀하의 마일리지가 다를 수 있습니다.

마지막으로 RAII는 C ++에서 잘 확립 된 관용구이고 개발자가 수많은 Scoped...클래스 를 작성해야하는 부담을 덜기 위해 이러한 종류의 결정적 정리를 용이하게하는 ScopeGuardBoost.ScopeExit 와 같은 라이브러리가 있습니다 .


8
C #에는 인터페이스를 using구현하는 모든 개체를 자동으로 정리 하는 문이 IDisposable있습니다. 따라서 틀릴 수는 있지만 올바르게하는 것은 매우 쉽습니다.
Robert Harvey

18
try/finally컴파일러가 try/finally구문을 노출하지 않고 클래스 기반을 통해 액세스 할 수있는 유일한 방법 이기 때문에 구문으로 컴파일러가 구현 한 디자인 관용구를 사용하여 임시 상태 변경 취소를 처리하기 위해 완전히 새로운 클래스를 작성해야 함 디자인 관용구는 "이점"이 아닙니다. 추상화 반전의 정의입니다.
메이슨 휠러

15
@MasonWheeler-음, 새 클래스를 작성하는 것이 장점이라고 말하지 않았습니다. 나는 그것이 단점이라고 말했다. 균형에, 그러나, 나는에 RAII를 선호하는 사용 finally. 내가 말했듯이 마일리지는 다를 수 있습니다.
Josh Kelley

7
@JoshKelley : '좋은 C ++ 디자인은 이런 종류의 최적화에 많이 의존합니다.' 외부 코드의 수병을 작성하고 컴파일러 최적화에 의존하는 것은 좋은 디자인 ?! IMO는 좋은 디자인의 대립입니다. 좋은 디자인의 기본은 간결하고 쉽게 읽을 수있는 코드입니다. 디버그, 유지 관리 등이 적습니다. 코드 덩어리를 작성 하지 말고 컴파일러를 사용하여 모든 것을 제거해야합니다. 전혀 이해가 안되는 IMO입니다!
벡터

14
@Mikey : 코드베이스 전체에 클린업 코드 (또는 클린업이 발생해야한다는 사실)를 복제하는 것이 "간결하고"쉽게 읽을 수 있는가? RAII를 사용하면 이러한 코드를 한 번 작성하면 모든 위치에 자동으로 적용됩니다.
Mankarse

54

에서 C가 제공 ++하지 않는 이유는 "마지막으로"구성? 에서 비얀 스트로브 스트 룹의 C ++ 스타일과 기술 자주 묻는 질문 :

C ++은 거의 항상 더 나은 대안을 지원하기 때문에 "자원 획득은 초기화"기술 (TC ++ PL3 섹션 14.4)입니다. 기본 아이디어는 로컬 객체의 소멸자가 리소스를 해제 할 수 있도록 로컬 객체로 리소스를 나타내는 것입니다. 그렇게하면 프로그래머가 리소스를 해제하는 것을 잊을 수 없습니다.


5
그러나 C ++에만 해당되는 기술에 대해서는 아무것도 없습니다. 객체, 생성자 및 소멸자를 사용하여 모든 언어로 RAII를 수행 할 수 있습니다. 그것은 좋은 기술이지만, RAII 단지 기존 것을 의미하지는 않습니다 finally, 구조가 영원히 항상 쓸모 에도 불구하고 Strousup가 무슨 말을. "예외 안전 코드"를 작성하는 것이 C ++에서 큰 문제라는 사실은 그 증거입니다. 지옥, C #을 모두 소멸자를 가지고 있으며 finally, 그들은 모두 익숙해.
Tacroy

28
@Tacroy : C ++은 결정 론적 소멸자 를 가진 주류 언어 중 하나입니다 . C # "소멸자"는이 목적에 쓸모가 없으며 RAII를 사용하려면 "사용"블록을 수동으로 작성해야합니다.
Nemanja Trifunovic 17 년

15
@Mikey 당신은 "왜 C ++이"최종 "구조를 제공하지 않습니까?" Stroustrup에서 직접 무엇을 더 요청할 수 있습니까? 즉 입니다 이유.

5
당신은 예외가 그것을 던져 때 코드가 특정 누출되지 자원을 잘 행동에 대해 걱정하는 경우 @Mikey, 당신이 하는 예외에게 안전한 코드를 작성하려고 / 예외 안전에 대한 걱정. 당신은 그것을 부르지 않고 다른 도구를 사용할 수 있기 때문에 다르게 구현합니다. 그러나 C ++ 사람들은 예외 안전에 대해 이야기 할 때 정확히 이야기합니다.

19
@ Kaz : 소멸자에서 정리를 한 번만 기억하면 그때부터 객체를 사용합니다. 할당하는 작업을 사용할 때마다 finally 블록에서 정리를 수행해야합니다.
deworde

19

C ++에없는 이유는 C ++에 필요하지 finally않기 때문입니다. finally예외가 발생했는지 여부에 관계없이 일부 코드를 실행하는 데 사용되며 거의 항상 일종의 정리 코드입니다. C ++에서이 정리 코드는 관련 클래스의 소멸자에 있어야하며 소멸자는 항상 finally블록 처럼 호출 됩니다. 정리에 소멸자를 사용하는 관용구를 RAII 라고 합니다.

C ++ 커뮤니티 내에서 '예외 안전'코드에 대해 더 많은 이야기가있을 수 있지만 예외가있는 다른 언어에서도 거의 동일하게 중요합니다. '예외 안전'코드의 요점은 호출하는 함수 / 메소드에서 예외가 발생하면 코드가 어떤 상태로 유지되는지 생각하는 것입니다.
C ++에서는 '예외 안전'코드가 약간 더 중요합니다. C ++에는 예외로 인해 분리 된 개체를 처리하는 자동 가비지 수집 기능이 없기 때문입니다.

예외 안전이 C ++ 커뮤니티에서 더 논의되는 이유는 아마도 C ++에서는 언어에 기본 안전망이 적기 때문에 무엇이 잘못 될 수 있는지 더 잘 알아야한다는 사실에서 비롯된 것입니다.


2
참고 : C ++에 결정 론적 소멸자가 있다고 주장하지 마십시오. Object Pascal / Delphi는 결정적 소멸자를 가지고 있지만 '최종'을 지원합니다. 아래 첫 번째 주석에서 설명한 아주 좋은 이유가 있습니다.
벡터

13
@Mikey : finallyC ++ 표준 에 추가 할 제안이 없었기 때문에 C ++ 커뮤니티가 the absence of finally문제 가되지 않는다고 결론 내리는 것이 안전하다고 생각 합니다. finallyC ++이 가지고있는 일관된 결정 론적 파괴 가 없는 대부분의 언어 . 델파이가 둘 다 가지고 있다는 것을 알지만, 어느 것이 먼저 있었는지 알 수있을만큼 역사를 잘 모릅니다.
Bart van Ingen Schenau

3
Dephi는 스택 기반 객체를 지원하지 않습니다. 스택 기반의 힙 기반 및 객체 참조 만 가능합니다. 따라서 적절한 경우 소멸자 등을 명시 적으로 호출하려면 '마지막'이 필요합니다.
벡터

2
C ++에는 분명히 필요하지 않은 많은 부패가 있기 때문에 이것이 정답이 될 수는 없습니다.
Kaz

15
지난 20 년 동안 나는 언어를 사용하고 그 언어를 사용하는 다른 사람들과 함께 일한 적이 있는데, "언제나 언어에 finally" 가 있기를 바란다 "고 말하는 C ++ 프로그래머를 만나 본 적이 없다 . 더 쉽게 접근 할 수 있었던 작업을 기억할 수 없었습니다.
로봇 고트

12

다른 사람들은 RAII를 해결책으로 논의했습니다. 완벽하게 좋은 솔루션입니다. 그러나 그것이 그들이 finally원하는 것이기 때문에 추가하지 않은 이유 를 실제로 다루지 는 않습니다 . 이에 대한 대답은 C ++의 디자인과 개발에 더 근본적입니다. C ++의 개발 과정에서 관련된 사람들은 많은 소란없이 다른 기능을 사용하여 달성 할 수있는 디자인 기능의 도입에 강하게 저항했으며 특히 도입이 필요한 경우 이전 코드를 호환하지 않을 수있는 새로운 키워드 RAII는 매우 기능적인 대안을 제공하고 finally실제로 finallyC ++ 11에서 직접 롤백 할 수 있기 때문에 호출 할 필요가 거의 없습니다.

Finally소멸자의 생성자에 전달 된 함수를 호출 하는 클래스 를 작성하기 만하면됩니다. 그런 다음이 작업을 수행 할 수 있습니다

try
{
    Finally atEnd([&] () { database.close(); });

    database.doRisky();
}

그러나 대부분의 네이티브 C ++ 프로그래머는 일반적으로 깔끔하게 디자인 된 RAII 객체를 선호합니다.


3
람다에서 참조 캡처가 누락되었습니다. 이어야한다 Finally atEnd([&] () { database.close(); });또한, 나는 다음과 같은 더 나은 상상 : { Finally atEnd(...); try {...} catch(e) {...} }(나는 그것이 catch 블록 후 실행하도록 시도 블록 밖으로 종료자를 해제.)
토마스 대한 수정 사항

2

try / catch 블록을 사용하지 않더라도 "트랩"패턴을 사용할 수 있습니다.

필요한 범위에 간단한 개체를 넣습니다. 이 객체의 소멸자에 "완료"논리를 넣으십시오. 무엇이든, 스택이 풀리면 오브젝트의 소멸자가 호출되고 사탕을 얻습니다.


1
이것은 질문에 대답하지 않으며 단순히 결국 그렇게 나쁜 생각이 아니라는 것을 증명 합니다.
Vector

2

글쎄, finallyLambdas를 사용하여 일종의 roll-your-own 을 사용할 수 있습니다.

{
    FILE *file = fopen("test","w");

    finally close_the_file([&]{
        cout << "We're closing the file in a pseudo-finally clause." << endl;
        fclose(file);
    });
}

이 기사를 참조 하십시오 .


-2

RAII가의 상위 집합이라는 주장에 동의하지 않습니다 finally. RAII의 아킬레스 건은 간단합니다. 예외입니다. RAII는 소멸자로 구현되며 C ++에서는 소멸자를 버리는 것이 항상 잘못되었습니다. 즉 정리 코드가 필요할 때 RAII를 사용할 수 없습니다. finally반면에 구현 된 경우 finally블록 에서 버리는 것이 합법적이지 않다고 믿을 이유가 없습니다 .

다음과 같은 코드 경로를 고려하십시오.

void foo() {
    try {
        ... stuff ...
        complex_cleanup();
    } catch (A& a) {
        handle_a(a);
        complex_cleanup();
        throw;
    } catch (B& b) {
        handle_b(b);
        complex_cleanup();
        throw;
    } catch (...) {
        handle_generic();
        complex_cleanup();
        throw;
    }
}

finally우리가 쓸 수 있다면 :

void foo() {
    try {
        ... stuff ...
    } catch (A& a) {
        handle_a(a);
        throw;
    } catch (B& b) {
        handle_b(b);
        throw;
    } catch (...) {
        handle_generic();
        throw;
    } finally {
        complex_cleanup();
    }
}

그러나 RAII를 사용하여 동등한 동작을 얻는 방법은 없습니다.

누군가 C ++ 에서이 작업을 수행하는 방법을 알고 있다면 대답에 매우 관심이 있습니다. 예를 들어, 특정 기능이나 무언가가있는 단일 클래스에서 모든 예외를 상속받는 것과 같이 의존하는 것에 만족합니다.


1
두 번째 예제에서 complex_cleanup던질 수 있다면 RAII / 소멸자와 마찬가지로 두 개의 잡히지 않은 예외가 한 번에 비행하는 경우가 있으며 C ++은이를 허용하지 않습니다. 원래 예외를 complex_cleanup보려면 RAII / 소멸자와 마찬가지로 예외를 방지해야합니다. complex_cleanup예외를 보고 싶다면 중첩 된 try / catch 블록을 사용할 수 있다고 생각합니다. 비록 접하기 어렵고 주석에 맞추기가 어렵 기 때문에 별도의 질문의 가치가 있습니다.
Josh Kelley 1

RAII를 사용하여 첫 번째 예와 동일한 동작을보다 안전하게 얻고 싶습니다. 추정 finally블록에서 던지는 것은 catchWRT 기내 예외에서 호출 하는 것과 동일하게 작동합니다 std::terminate. 문제는 " finallyC ++에서 왜 아니요 "입니까? 답은 "필요하지 않다 ... RAII FTW!" 내 요점은 그렇습니다. RAII는 메모리 관리와 같은 간단한 경우에는 적합하지만 예외 문제가 해결 될 때까지 범용 솔루션이 되기에는 너무 많은 생각 / 오버 헤드 / 관심 / 재 설계가 필요합니다.
MadScientist

3
나는 당신의 요점을 이해합니다-소멸자와 관련이있는 합법적 인 문제가 있지만 드물습니다. RAII + 예외가 해결되지 않은 문제이거나 RAII가 범용 솔루션이 아니라고 말하는 것은 대부분의 C ++ 개발자의 경험과 일치하지 않습니다.
Josh Kelley

1
소멸자에서 예외를 제기해야한다고 생각되면 무언가 잘못하고있는 것입니다. 아마도 다른 곳에서 포인터를 사용하지 않아도됩니다.
벡터

1
이것은 의견에 너무 관여합니다. 그것에 대해 질문 게시 : 당신이 RAII 모델을 사용하여 C ++에서이 시나리오를 처리 할 방법을 ... ... 작동하지 않는 당신은해야한다, 다시 귀하의 의견을 직접 : 형 @ 멤버의 이름은 당신이 얘기 귀하의 의견의 시작 부분에. 댓글이 자신의 게시물에 있으면 모든 내용에 대한 알림을받을 수 있지만 댓글을 달지 않으면 다른 사람은 알림을받지 않습니다.
벡터
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.