development

생성자에서 예외 발생

big-blog 2020. 3. 31. 08:18
반응형

생성자에서 예외 발생


생성자로부터 예외를 던지는 것에 대해 동료와 토론 중이며 피드백이 필요하다고 생각했습니다.

설계 관점에서 생성자에서 예외를 throw해도 괜찮습니까?

POSIX 뮤텍스를 클래스로 래핑한다고 가정하면 다음과 같습니다.

class Mutex {
public:
  Mutex() {
    if (pthread_mutex_init(&mutex_, 0) != 0) {
      throw MutexInitException();
    }
  }

  ~Mutex() {
    pthread_mutex_destroy(&mutex_);
  }

  void lock() {
    if (pthread_mutex_lock(&mutex_) != 0) {
      throw MutexLockException();
    }
  }

  void unlock() {
    if (pthread_mutex_unlock(&mutex_) != 0) {
      throw MutexUnlockException();
    }
  }

private:
  pthread_mutex_t mutex_;
};

내 질문은 이것이 표준 방법입니까? 경우 생성하기 때문에 pthread mutex_init호출이 실패 뮤텍스 객체는 뮤텍스가 생성되지 않습니다 예외 보장하지만 던지는 때문에 사용할 수 없습니다.

오히려 Mutex 클래스에 대한 멤버 함수 init을 작성하고의 pthread mutex_init리턴에 따라 bool을 리턴하는 호출을 작성해야합니까 pthread mutex_init? 이렇게하면 저수준 객체에 예외를 사용할 필요가 없습니다.


예, 실패한 생성자에서 예외를 던지는 것이 표준 방법입니다. 자세한 내용 은 실패한 생성자 처리에 대한 FAQ 를 참조하십시오. init () 메소드를 사용하는 것도 효과가 있지만, mutex의 객체를 만드는 모든 사람은 init ()를 호출해야한다는 것을 기억해야합니다. RAII 원칙 에 위배된다고 생각합니다 .


생성자에서 예외가 발생하면 생성자 이니셜 라이저 목록에서 해당 예외를 잡아야하는 경우 try / catch 구문을 사용해야합니다.

예 :

func::func() : foo()
{
    try {...}
    catch (...) // will NOT catch exceptions thrown from foo constructor
    { ... }
}

vs.

func::func()
    try : foo() {...}
    catch (...) // will catch exceptions thrown from foo constructor
    { ... }

생성자 실패를 처리하는 가장 좋은 방법은 예외를 던지는 것입니다. 특히 객체를 반만 구성한 다음 어떤 종류의 플래그 변수를 테스트하여 구성 오류를 감지하기 위해 클래스 사용자에게 의존하지 않아야합니다.

관련하여, 뮤텍스 오류를 처리하기 위해 몇 가지 다른 예외 유형이 있다는 사실이 약간 걱정됩니다. 상속은 훌륭한 도구이지만 과도하게 사용될 수 있습니다. 이 경우 아마도 유익한 오류 메시지를 포함하는 단일 MutexError 예외를 선호합니다.


#include <iostream>

class bar
{
public:
  bar()
  {
    std::cout << "bar() called" << std::endl;
  }

  ~bar()
  {
    std::cout << "~bar() called" << std::endl;

  }
};
class foo
{
public:
  foo()
    : b(new bar())
  {
    std::cout << "foo() called" << std::endl;
    throw "throw something";
  }

  ~foo()
  {
    delete b;
    std::cout << "~foo() called" << std::endl;
  }

private:
  bar *b;
};


int main(void)
{
  try {
    std::cout << "heap: new foo" << std::endl;
    foo *f = new foo();
  } catch (const char *e) {
    std::cout << "heap exception: " << e << std::endl;
  }

  try {
    std::cout << "stack: foo" << std::endl;
    foo f;
  } catch (const char *e) {
    std::cout << "stack exception: " << e << std::endl;
  }

  return 0;
}

출력 :

heap: new foo
bar() called
foo() called
heap exception: throw something
stack: foo
bar() called
foo() called
stack exception: throw something

소멸자가 호출되지 않으므로 생성자에서 예외가 발생해야하는 경우 많은 작업 (예 : 정리?)을 수행해야합니다.


생성자에서 던져도 괜찮지 만, main 이 시작된 그리고 완료되기 전에 객체가 생성되었는지 확인해야합니다 .

class A
{
public:
  A () {
    throw int ();
  }
};

A a;     // Implementation defined behaviour if exception is thrown (15.3/13)

int main ()
{
  try
  {
    // Exception for 'a' not caught here.
  }
  catch (int)
  {
  }
}

일반적으로 프로젝트에서 예외 데이터를 사용하여 불량 데이터를 양호한 데이터와 구별하는 경우 생성자에서 예외를 발생시키는 것이 발생하지 않는 것보다 나은 솔루션입니다. 예외가 발생하지 않으면 객체는 좀비 상태로 초기화됩니다. 이러한 객체는 객체가 올바른지 여부를 나타내는 플래그를 노출해야합니다. 이 같은:

class Scaler
{
    public:
        Scaler(double factor)
        {
            if (factor == 0)
            {
                _state = 0;
            }
            else
            {
                _state = 1;
                _factor = factor;
            }
        }

        double ScaleMe(double value)
        {
            if (!_state)
                throw "Invalid object state.";
            return value / _factor;
        }

        int IsValid()
        {
            return _status;
        }

    private:
        double _factor;
        int _state;

}

이 방법의 문제점은 발신자 측에 있습니다. 클래스의 모든 사용자는 실제로 객체를 사용하기 전에 if를 수행해야합니다. 이것은 버그를 요구합니다. 계속하기 전에 조건을 테스트하는 것을 잊는 것보다 더 간단한 것은 없습니다.

생성자에서 예외를 발생시키는 경우 객체를 생성하는 엔티티는 즉시 문제를 처리해야합니다. 스트림 아래의 객체 소비자는 객체가 얻은 사실만으로도 객체가 100 % 작동한다고 가정 할 수 있습니다.

이 논의는 여러 방향으로 계속 될 수 있습니다.

예를 들어, 검증 문제로 예외를 사용하는 것은 좋지 않습니다. 이를 수행하는 한 가지 방법은 팩토리 클래스와 함께 Try 패턴입니다. 이미 팩토리를 사용하고 있다면 두 가지 방법을 작성하십시오.

class ScalerFactory
{
    public:
        Scaler CreateScaler(double factor) { ... }
        int TryCreateScaler(double factor, Scaler **scaler) { ... };
}

이 솔루션을 사용하면 잘못된 데이터로 생성자를 입력하지 않고도 팩토리 메소드의 리턴 값으로 상태 플래그를 제자리에서 얻을 수 있습니다.

두 번째는 자동화 된 테스트로 코드를 다루는 것입니다. 이 경우 예외를 발생시키지 않는 개체를 사용하는 모든 코드는 추가 테스트로 처리해야합니다. IsValid () 메서드가 false를 반환 할 때 올바르게 작동하는지 여부입니다. 이것은 좀비 상태에서 객체를 초기화하는 것이 좋지 않다는 것을 잘 설명합니다.


외에도에서 당신이 던질 필요가 없다는 사실 때문에 특정한 경우에 생성자에서 pthread_mutex_lock실제로 반환 EINVAL을 뮤텍스가 초기화되어 있지 않은 경우 당신이 호출 후 던질 수 lock에서 수행 될 때 std::mutex:

void
lock()
{
  int __e = __gthread_mutex_lock(&_M_mutex);

  // EINVAL, EAGAIN, EBUSY, EINVAL, EDEADLK(may)
  if (__e)
__throw_system_error(__e);
}

다음 일반적으로 생성자에서 던지는 괜찮 에 대한 취득 건설 기간 동안 오류 및 준수 RAII (자원 인수-IS-초기화) 프로그래밍 패러다임.

RAII 에서이 예를 확인하십시오.

void write_to_file (const std::string & message) {
    // mutex to protect file access (shared across threads)
    static std::mutex mutex;

    // lock mutex before accessing file
    std::lock_guard<std::mutex> lock(mutex);

    // try to open file
    std::ofstream file("example.txt");
    if (!file.is_open())
        throw std::runtime_error("unable to open file");

    // write message to file
    file << message << std::endl;

    // file will be closed 1st when leaving scope (regardless of exception)
    // mutex will be unlocked 2nd (from lock destructor) when leaving
    // scope (regardless of exception)
}

이 진술에 집중하십시오 :

  1. static std::mutex mutex
  2. std::lock_guard<std::mutex> lock(mutex);
  3. std::ofstream file("example.txt");

첫 번째 진술은 RAII와 noexcept입니다. (2)에서는 RAII가 적용되어 lock_guard실제로 가능 하다는 것이 명백 throw하지만, (3)에서는 ofstreamRAII가 아닌 것으로 보입니다 . 플래그 is_open()를 확인하는 호출 통해 객체 상태 를 확인해야하기 때문 failbit입니다.

언뜻보기에는 표준 방식 에 미정 인 것으로 보이며 첫 번째 경우 std::mutex에는 OP 구현과 달리 초기화가 발생 하지 않습니다. 두 번째 경우에는에서 던지는 것을 던지며 std::mutex::lock세 번째 경우에는 전혀 던지지 않습니다.

차이점을 주목하십시오 :

(1) 정적으로 선언 될 수 있고 실제로 멤버 변수로 선언 될 것입니다. (2) 실제로 멤버 변수로 선언 될 것으로 예상되지 않을 것입니다. (3) 멤버 변수로 선언 될 것으로 예상되며 기본 리소스는 항상 사용 가능한 것은 아닙니다.

이 모든 형태는 RAII입니다 . 이 문제를 해결하려면 RAII 를 분석해야합니다 .

  • 리소스 : 객체
  • 획득 (할당) : 생성중인 객체
  • 초기화 : 객체가 변하지 않는 상태입니다

이것은 당신이 건설중인 모든 것을 초기화하고 연결할 필요가 없습니다. 예를 들어 네트워크 클라이언트 객체를 생성 할 때는 실패시 느리게 작동하기 때문에 실제로 생성시 서버에 연결하지 않습니다. 대신 connect그렇게 하는 함수를 작성하십시오 . 반면에 버퍼를 만들거나 상태를 설정할 수 있습니다.

따라서 문제는 초기 상태를 정의하는 것으로 요약됩니다. 귀하의 경우 초기 상태가 뮤텍스 인 경우 초기화해야합니다 . 대조적으로, 그때처럼 초기화하지 말고 뮤텍스가 생성std::mutex 될 때 불변 상태를 정의하는 것이 좋습니다. 여하튼, 불변 인은 반드시 객체 객체의 상태에 의해 반드시 타협되지는 않습니다. 왜냐하면 mutex_객체 공개 메소드 와를 통해 locked그리고를 unlocked통해 변경 되기 때문 입니다.MutexMutex::lock()Mutex::unlock()

class Mutex {
private:
  int e;
  pthread_mutex_t mutex_;

public:
  Mutex(): e(0) {
  e = pthread_mutex_init(&mutex_);
  }

  void lock() {

    e = pthread_mutex_lock(&mutex_);
    if( e == EINVAL ) 
    { 
      throw MutexInitException();
    }
    else (e ) {
      throw MutexLockException();
    }
  }

  // ... the rest of your class
};

생성자로부터 예외를 발생시키지 않는 유일한 시간은 프로젝트에 예외 사용에 대한 규칙이있는 경우입니다 (예 : Google 은 예외를 좋아하지 않습니다). 이 경우 생성자에서 예외를 다른 곳보다 더 많이 사용하고 싶지 않으며 대신 일종의 init 메소드가 있어야합니다.


여기에 모든 답변 Init에 덧붙여서 Ctor가 아닌 클래스의 메소드 에서 예외를 던지기를 원할 수있는 매우 구체적인 이유 / 시나리오를 언급했습니다 .

이 예제 (시나리오)에서는 std::unique_ptr클래스의 포인터 데이터 멤버에 "스마트 포인터"(즉, )를 사용하지 않는다고 가정한다고 미리 언급합니다 .

요점 : 당신이 클래스의 Dtor가 Init()메소드를 던진 예외를 잡은 후에 (이 경우) 호출 할 때 "행동을 취하기"를 원한다면 Ctor에서 예외를 던져서는 안됩니다. "반 구운"객체에서 Ctor에 대한 Dtor 호출이 호출되지 않습니다.

내 요점을 설명하려면 아래 예를 참조하십시오.

#include <iostream>

using namespace std;

class A
{
    public:
    A(int a)
        : m_a(a)
    {
        cout << "A::A - setting m_a to:" << m_a << endl;
    }

    ~A()
    {
        cout << "A::~A" << endl;
    }

    int m_a;
};

class B
{
public:
    B(int b)
        : m_b(b)
    {
        cout << "B::B - setting m_b to:" << m_b << endl;
    }

    ~B()
    {
        cout << "B::~B" << endl;
    }

    int m_b;
};

class C
{
public:
    C(int a, int b, const string& str)
        : m_a(nullptr)
        , m_b(nullptr)
        , m_str(str)
    {
        m_a = new A(a);
        cout << "C::C - setting m_a to a newly A object created on the heap (address):" << m_a << endl;
        if (b == 0)
        {
            throw exception("sample exception to simulate situation where m_b was not fully initialized in class C ctor");
        }

        m_b = new B(b);
        cout << "C::C - setting m_b to a newly B object created on the heap (address):" << m_b << endl;
    }

    ~C()
    {
        delete m_a;
        delete m_b;
        cout << "C::~C" << endl;
    }

    A* m_a;
    B* m_b;
    string m_str;
};

class D
{
public:
    D()
        : m_a(nullptr)
        , m_b(nullptr)
    {
        cout << "D::D" << endl;
    }

    void InitD(int a, int b)
    {
        cout << "D::InitD" << endl;
        m_a = new A(a);
        throw exception("sample exception to simulate situation where m_b was not fully initialized in class D Init() method");
        m_b = new B(b);
    }

    ~D()
    {
        delete m_a;
        delete m_b;
        cout << "D::~D" << endl;
    }

    A* m_a;
    B* m_b;
};

void item10Usage()
{
    cout << "item10Usage - start" << endl;

    // 1) invoke a normal creation of a C object - on the stack
    // Due to the fact that C's ctor throws an exception - its dtor
    // won't be invoked when we leave this scope
    {
        try
        {
            C c(1, 0, "str1");
        }
        catch (const exception& e)
        {
            cout << "item10Usage - caught an exception when trying to create a C object on the stack:" << e.what() << endl;
        }
    }

    // 2) same as in 1) for a heap based C object - the explicit call to 
    //    C's dtor (delete pc) won't have any effect
    C* pc = 0;
    try
    {
        pc = new C(1, 0, "str2");
    }
    catch (const exception& e)
    {
        cout << "item10Usage - caught an exception while trying to create a new C object on the heap:" << e.what() << endl;
        delete pc; // 2a)
    }

    // 3) Here, on the other hand, the call to delete pd will indeed 
    //    invoke D's dtor
    D* pd = new D();
    try
    {
        pd->InitD(1,0);
    }
    catch (const exception& e)
    {
        cout << "item10Usage - caught an exception while trying to init a D object:" << e.what() << endl;
        delete pd; 
    }

    cout << "\n \n item10Usage - end" << endl;
}

int main(int argc, char** argv)
{
    cout << "main - start" << endl;
    item10Usage();
    cout << "\n \n main - end" << endl;
    return 0;
}

나는 그것이 권장되는 접근법이 아니라 단지 다른 관점을 공유하기를 원한다고 다시 언급 할 것이다.

또한 코드의 일부 인쇄물에서 볼 수 있듯이 Scott Meyers (1 판)의 환상적인 "더 효과적인 C ++"항목 10을 기반으로합니다.

도움이 되길 바랍니다.

건배,

사람.


나는 전문적인 수준에서 C ++을 사용하지는 않았지만 제 생각에는 생성자에서 예외를 throw하는 것이 좋습니다. .Net에서 (필요한 경우) 그렇게합니다. 확인 링크를. 관심이 있으실 것입니다.

참고 URL : https://stackoverflow.com/questions/810839/throwing-exceptions-from-constructors

반응형