Auslösen von Ausnahmen von Konstruktoren

Lesezeit: 10 Minuten

Benutzer-Avatar
lkristjansen

Ich habe eine Debatte mit einem Kollegen über das Auslösen von Ausnahmen von Konstruktoren und dachte, ich hätte gerne etwas Feedback.

Ist es aus gestalterischer Sicht in Ordnung, Ausnahmen von Konstruktoren auszulösen?

Nehmen wir an, ich verpacke einen POSIX-Mutex in einer Klasse, das würde ungefähr so ​​​​aussehen:

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_;
};

Meine Frage ist, ist dies die Standardmethode? Denn wenn die pthread mutex_init Aufruf fehlschlägt, ist das Mutex-Objekt unbrauchbar, sodass durch das Auslösen einer Ausnahme sichergestellt wird, dass der Mutex nicht erstellt wird.

Soll ich lieber eine Member-Funktion init für die Mutex-Klasse erstellen und aufrufen pthread mutex_init innerhalb dessen würde ein boolscher Wert basierend auf zurückgegeben werden pthread mutex_initRückkehr? Auf diese Weise muss ich keine Ausnahmen für ein solches Low-Level-Objekt verwenden.

  • Noch ein Link zum verwandten Thema: writeulearn.com/exception-constructor

    Benutzer4215465

    4. November 2014 um 17:38 Uhr


  • Nun, es ist in Ordnung, von Ctors genauso viel zu werfen wie von jeder anderen Funktion, aber Sie sollten von jeder Funktion aus vorsichtig werfen.

    – g24l

    26. Dezember 2015 um 23:04 Uhr


  • Etwas Unabhängiges: Warum entfernen Sie nicht Ihre Lock/Unlock-Methoden und sperren den Mutex direkt im Konstruktor und entsperren ihn im Destruktor? Auf diese Weise wird einfach eine Auto-Variable in einem Gültigkeitsbereich automatisch gesperrt/entsperrt, ohne dass Ausnahmen, Rückgaben usw. berücksichtigt werden müssen. Siehe std::lock_guard für eine ähnliche Implementierung.

    – Laurent Gregoire

    7. Juli 2016 um 8:25 Uhr


  • Wenn Ihre Konstruktion fehlschlägt und eine Ausnahme auslöst, wird ~Mutex() nicht aufgerufen und mutex_ wird nicht bereinigt. Lösen Sie keine Ausnahmen in Konstruktoren aus.

    – Andy Krouwel

    10. Februar 2017 um 14:06 Uhr

  • @LaurentGrégoire: Das Erstellen und Sperren eines Mutex im Konstruktor wäre sinnlos, da sonst niemand einen Verweis auf diesen Mutex hätte, sodass nichts geschützt würde. Du wollen lock und unlock damit Ihr Mutex-Typ funktioniert std::lock_guard; er implementiert neu std::mutexnicht std::lock_guard hier, und es gibt einen Grund, warum die beiden Klassen in der C++-Standardbibliothek getrennt sind.

    – ShadowRanger

    3. April 2017 um 12:26 Uhr

Benutzer-Avatar
Naveen

Ja, das Auslösen einer Ausnahme vom fehlgeschlagenen Konstruktor ist die Standardmethode, dies zu tun. Lesen Sie diese FAQ über Umgang mit einem Konstruktor, der fehlschlägt für mehr Informationen. Eine init()-Methode zu haben, wird auch funktionieren, aber jeder, der das Objekt von mutex erstellt, muss daran denken, dass init() aufgerufen werden muss. Ich fühle, dass es gegen die geht RAII Prinzip.

  • In den meisten Situationen. Vergessen Sie Dinge wie std::fstream nicht. Bei einem Fehler wird immer noch ein Objekt erstellt, aber da wir immer den Zustand des Objekts testen, funktioniert es normalerweise gut. Ein Objekt, das einen natürlichen Zustand hat, der unter normaler Verwendung getestet wird, muss also möglicherweise nicht geworfen werden.

    – Martin York

    1. Mai 2009 um 14:49 Uhr

  • @Widor: Vielen Dank für die Überprüfung meiner vorgeschlagenen Bearbeitung Nr. 278978. Darf ich noch eine Frage zum Bearbeiten stellen? Die Antwort, an die dieser Kommentar angehängt ist, enthält einen veralteten Hyperlink. Um dies zu beheben, möchte er genau ein Zeichen ändern und in der URL „#faq-17.2“ durch „#faq-17.8“ ersetzen. Die Software von Stackoverflow erfordert jedoch, dass eine Änderung, die von einem Benutzer mit geringer Reputation wie mir eingereicht wird, mindestens sechs Zeichen ändert. Ziemlich offensichtlich möchte der defekte Link behoben werden, und es ist einfach kein Fix mit sechs Zeichen. Weißt du bitte, wie ich das beheben kann?

    – thb

    3. Juni 2012 um 16:51 Uhr

  • Beachten Sie in diesem speziellen Fall nicht wirklich, dass sein Mutex-Destruktor niemals aufgerufen wird, wodurch möglicherweise der pthread-Mutex verloren geht. Die Lösung dafür ist die Verwendung eines intelligenten Zeigers für den pthread-Mutex, besser noch die Verwendung von Boost-Mutexen oder std::mutex, kein Grund, weiterhin alte Betriebssystemkonstrukte im funktionalen Stil zu verwenden, wenn es bessere Alternativen gibt.

    Benutzer90843

    21. Oktober 2012 um 7:33 Uhr


  • @Martin York: Ich bin mir nicht sicher, ob std::fstream ein gutes Beispiel ist. Ja. Es stützt sich auf die Fehlerprüfung nach dem Konstruktor. Aber sollte es? Es ist ein schreckliches Design, das aus einer Version von C++ stammt, in der es Konstruktoren verboten war, Ausnahmen auszulösen.

    – Robin Davis

    14. Februar um 3:04 Uhr


Benutzer-Avatar
Ferruccio

Wenn Sie eine Ausnahme von einem Konstruktor auslösen, denken Sie daran, dass Sie die try/catch-Syntax der Funktion verwenden müssen, wenn Sie diese Ausnahme in einer Konstruktor-Initialisierungsliste abfangen müssen.

z.B

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
    { ... }

  • Es sollte beachtet werden, dass Ausnahmen, die von der Konstruktion eines Unterobjekts ausgelöst werden, nicht unterdrückt werden können: gotw.ca/gotw/066.htm

    – Eric Malenfant

    1. Mai 2009 um 13:36 Uhr

Das Auslösen einer Ausnahme ist die beste Möglichkeit, mit Konstruktorfehlern umzugehen. Sie sollten insbesondere vermeiden, ein Objekt halb zu konstruieren und sich dann darauf zu verlassen, dass Benutzer Ihrer Klasse Konstruktionsfehler erkennen, indem sie Flag-Variablen irgendeiner Art testen.

In einem ähnlichen Punkt beunruhigt mich die Tatsache, dass Sie mehrere verschiedene Ausnahmetypen für den Umgang mit Mutex-Fehlern haben. Vererbung ist ein großartiges Werkzeug, aber es kann überstrapaziert werden. In diesem Fall würde ich wahrscheinlich eine einzelne MutexError-Ausnahme bevorzugen, die möglicherweise eine informative Fehlermeldung enthält.

  • Ich würde Neils Argument zur Ausnahmehierarchie bestätigen – ein einzelner MutexError ist wahrscheinlich die bessere Wahl, es sei denn, Sie möchten einen Sperrfehler ausdrücklich anders behandeln. Wenn Sie zu viele Ausnahmetypen haben, kann es mühsam und fehleranfällig werden, sie alle abzufangen.

    – markh44

    1. Mai 2009 um 11:30 Uhr

  • Ich stimme zu, dass eine Art von Mutex-Ausnahme ausreicht. Und das wird auch die Fehlerbehandlung intuitiver machen.

    – lkristjansen

    1. Mai 2009 um 12:19 Uhr

#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;
}

die Ausgabe:

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

die Destruktoren werden nicht aufgerufen, wenn also eine Ausnahme in einem Konstruktor geworfen werden muss, ist eine Menge Zeug (z. B. Aufräumen?) zu erledigen.

Es ist in Ordnung, von Ihrem Konstruktor zu werfen, aber Sie sollten sicherstellen, dass Ihr Objekt danach konstruiert wird hauptsächlich begonnen hat und bevor es endet:

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)
  {
  }
}

Benutzer-Avatar
Michael Köhne

Sie würden nur dann KEINE Ausnahmen von Konstruktoren auslösen, wenn Ihr Projekt eine Regel gegen die Verwendung von Ausnahmen hat (z. B. Google mag keine Ausnahmen). In diesem Fall möchten Sie Ausnahmen in Ihrem Konstruktor genauso wenig verwenden wie anderswo, und Sie müssten stattdessen eine Art Init-Methode haben.

Zusätzlich zu allen Antworten hier dachte ich, einen ganz bestimmten Grund/Szenario zu erwähnen, in dem Sie es vielleicht vorziehen möchten, die Ausnahme von der Klasse auszulösen Init Methode und nicht vom Ctor (was natürlich der bevorzugte und gebräuchlichere Ansatz ist).

Ich werde im Voraus erwähnen, dass dieses Beispiel (Szenario) davon ausgeht, dass Sie keine “intelligenten Zeiger” verwenden (dh std::unique_ptr) für die Zeigerdatenelemente Ihrer Klasse.

Also auf den Punkt: Falls Sie möchten, dass der Dtor Ihrer Klasse “Aktionen ergreift”, wenn Sie ihn aufrufen, nachdem Sie (für diesen Fall) die Ausnahme abgefangen haben, die Ihre Init() method threw – Sie dürfen die Ausnahme von Ctor nicht auslösen, da ein Dtor-Aufruf für Ctors NICHT für “halbgebackene” Objekte aufgerufen wird.

Sehen Sie sich das folgende Beispiel an, um meinen Standpunkt zu demonstrieren:

#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;
}

Ich werde noch einmal erwähnen, dass dies nicht der empfohlene Ansatz ist, wollte nur einen zusätzlichen Standpunkt teilen.

Außerdem, wie Sie vielleicht an einigen Stellen im Code gesehen haben, basiert er auf Punkt 10 des fantastischen Buches „Effektiveres C++“ von Scott Meyers (1. Auflage).

1013240cookie-checkAuslösen von Ausnahmen von Konstruktoren

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy