web-dev-qa-db-ger.com

Ausnahmen von Konstruktoren auslösen

Ich habe eine Debatte mit einem Kollegen über das Werfen von Ausnahmen von Konstrukteuren und dachte, ich hätte gerne ein Feedback.

Ist es in Ordnung, konstruktiv gesehen Ausnahmen von Konstruktoren auszulösen?

Nehmen wir an, ich binde einen POSIX-Mutex in eine Klasse ein. 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? Wenn der Aufruf von pthread mutex_init Fehlschlägt, ist das Mutex-Objekt unbrauchbar, sodass durch 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 pthread mutex_init Aufrufen, in der ein Bool zurückgegeben wird, der auf der Rückkehr von pthread mutex_init Basiert? Auf diese Weise muss ich keine Ausnahmen für ein so niedriges Objekt verwenden.

258
lkristjansen

Ja, das Auslösen einer Ausnahme vom fehlgeschlagenen Konstruktor ist die Standardmethode. Lesen Sie dies FAQ about Behandeln eines fehlgeschlagenen Konstruktors für weitere Informationen. Eine init () -Methode funktioniert auch, aber jeder, der das Objekt von mutex erstellt, muss sich daran erinnern dieser init () muss aufgerufen werden, was meiner Meinung nach gegen das RAII Prinzip verstößt.

245
Naveen

Wenn Sie eine Ausnahme von einem Konstruktor auslösen, beachten Sie, dass Sie die Try/Catch-Syntax der Funktion verwenden müssen, wenn Sie diese Ausnahme in einer Konstruktorinitialisierungsliste 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
    { ... }
96
Ferruccio

Das Auslösen einer Ausnahme ist die beste Methode, um Konstruktorfehler zu beheben. Sie sollten insbesondere vermeiden, ein Objekt zur Hälfte zu konstruieren und sich dann darauf zu verlassen, dass Benutzer Ihrer Klasse Konstruktionsfehler erkennen, indem Sie Flag-Variablen einer Art testen.

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

34
anon
#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 einen Konstruktor geworfen werden muss, müssen viele Dinge erledigt werden (z. B. Aufräumen?).

15
Morris

Es ist in Ordnung, von Ihrem Konstruktor zu werfen, aber Sie sollten sicherstellen, dass Ihr Objekt erstellt wird, nachdem main gestartet wurde und bevor es beendet wird:

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)
  {
  }
}
14
Richard Corden

Abgesehen von der Tatsache , dass Sie in Ihrem speziellen Fall nicht aus dem Konstruktor werfen müssen, weil pthread_mutex_lock Tatsächlich ein zurückgibt [~ # ~] einval [~ # ~] wenn dein Mutex nicht initialisiert wurde und du kannst nach dem Aufruf auf lock wie in std::mutex gemacht:

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

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

dann ist im Allgemeinen Werfen von Konstrukteuren in Ordnung für Erfassungsfehler während der Konstruktion und in Übereinstimmung mit [~ # ~] raii [~ # ~] Programmierparadigma (Ressourcenerfassung ist Initialisierung).

Überprüfen Sie dies Beispiel für 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)
}

Konzentrieren Sie sich auf diese Aussagen:

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

Die erste Anweisung lautet RAII und noexcept. In (2) ist klar, dass RAII auf lock_guard Angewendet wird und tatsächlich throw kann, wohingegen in (3) ofstream nicht RAII zu sein scheint, da die Objekte angeben muss durch Aufrufen von is_open() überprüft werden, das das failbit -Flag überprüft.

Auf den ersten Blick scheint es unentschlossen zu sein, worauf es bei der Standardmethode und im ersten Fall std::mutex Nicht bei der Initialisierung ankommt, * Im Gegensatz zur OP-Implementierung *. Im zweiten Fall wirft es alles, was von std::mutex::lock Geworfen wird, und im dritten Fall gibt es überhaupt keinen Wurf.

Beachten Sie die Unterschiede:

(1) Kann als statisch deklariert werden und wird tatsächlich als Mitgliedsvariable deklariert. (2) Wird niemals als Mitgliedsvariable deklariert. (3) Wird voraussichtlich als Mitgliedsvariable deklariert, und die zugrunde liegende Ressource wird möglicherweise deklariert nicht immer verfügbar sein.

Alle diese Formen sind [~ # ~] raii [~ # ~] ; Um dies zu lösen, muss man [~ # ~] raii [~ # ~] analysieren.

  • Ressource: Ihr Objekt
  • Erfassung (Zuordnung): Ihr Objekt wird angelegt
  • Initialisierung: Ihr Objekt befindet sich in seinem unveränderlichen Zustand

Dies erfordert nicht, dass Sie alles auf der Konstruktion initialisieren und verbinden. Wenn Sie beispielsweise ein Netzwerk-Client-Objekt erstellen, stellen Sie beim Erstellen keine Verbindung zum Server her, da es sich um einen langsamen Vorgang mit Fehlern handelt. Sie würden stattdessen eine connect -Funktion schreiben, um genau das zu tun. Auf der anderen Seite können Sie die Puffer erstellen oder einfach den Status festlegen.

Daher läuft Ihr Problem darauf hinaus, Ihren Anfangszustand zu definieren. Wenn in Ihrem Fall Ihr Ausgangszustand ist, muss Mutex initialisiert werden , dann sollten Sie vom Konstruktor werfen. Im Gegensatz dazu ist es in Ordnung, dann nicht zu initialisieren (wie in std::mutex) Und Ihren unveränderlichen Zustand als zu definieren, wenn ein Mutex erstellt wird . . In jedem Fall wird die Invariante nicht notwendigerweise durch den Zustand ihres Mitgliedsobjekts gefährdet, da das mutex_ - Objekt zwischen locked und unlocked durch das Mutex public mutiert Methoden Mutex::lock() und 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
};
4
g24l

Wenn sich Ihr Projekt im Allgemeinen auf Ausnahmen stützt, um fehlerhafte Daten von guten Daten zu unterscheiden, ist es die bessere Lösung, eine Ausnahme vom Konstruktor auszulösen, als keine auszulösen. Wenn keine Ausnahme ausgelöst wird, wird das Objekt in einem Zombie-Status initialisiert. Ein solches Objekt muss ein Flag anzeigen, das angibt, ob das Objekt korrekt ist oder nicht. Etwas wie das:

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;

}

Problem bei diesem Ansatz ist auf der Anruferseite. Jeder Benutzer der Klasse müsste ein if ausführen, bevor er das Objekt tatsächlich verwendet. Dies ist eine Aufforderung zur Fehlerbehebung - es gibt nichts Einfacheres, als zu vergessen, eine Bedingung zu testen, bevor Sie fortfahren.

Wenn eine Ausnahme vom Konstruktor ausgelöst wird, muss die Einheit, die das Objekt erstellt, die Probleme sofort beheben. Objektverbraucher können davon ausgehen, dass das Objekt zu 100% betriebsbereit ist, wenn sie es erhalten haben.

Diese Diskussion kann in viele Richtungen fortgesetzt werden.

Zum Beispiel ist die Verwendung von Ausnahmen zur Validierung eine schlechte Praxis. Eine Möglichkeit ist ein Try-Pattern in Verbindung mit der Factory-Klasse. Wenn Sie bereits Fabriken verwenden, schreiben Sie zwei Methoden:

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

Mit dieser Lösung können Sie das Statusflag direkt als Rückgabewert der Factory-Methode abrufen, ohne jemals einen Konstruktor mit fehlerhaften Daten eingeben zu müssen.

Zweitens, wenn Sie den Code mit automatisierten Tests abdecken. In diesem Fall müsste jeder Code, der ein Objekt verwendet, das keine Ausnahmen auslöst, mit einem zusätzlichen Test abgedeckt werden - ob er korrekt funktioniert, wenn die IsValid () -Methode false zurückgibt. Dies erklärt recht gut, dass das Initialisieren von Objekten im Zombie-Status eine schlechte Idee ist.

4
Zoran Horvat

Das einzige Mal, dass Sie KEINE Ausnahmen von Konstruktoren auslösen würden, ist, wenn Ihr Projekt eine Regel gegen die Verwendung von Ausnahmen enthält (z. B. Google mag keine Ausnahmen). In diesem Fall möchten Sie Ausnahmen in Ihrem Konstruktor nicht mehr als irgendwo anders verwenden, und Sie müssten stattdessen eine Art Init-Methode verwenden.

3
Michael Kohne

Zu all den Antworten hier möchte ich noch einen ganz bestimmten Grund/ein bestimmtes Szenario hinzufügen, in dem Sie die Ausnahme möglicherweise lieber von der Init -Methode der Klasse auslösen möchten und nicht von der Ctor (die natürlich die bevorzugte ist) und allgemeinerer Ansatz).

Ich werde im Voraus erwähnen, dass in diesem Beispiel (Szenario) davon ausgegangen wird, dass Sie keine "intelligenten Zeiger" (d. H. - std::unique_ptr) Für die Zeigerdatenelemente Ihrer Klasse verwenden.

Also auf den Punkt: Falls Sie möchten, dass der Dtor Ihrer Klasse "Maßnahmen ergreift", wenn Sie ihn aufrufen, nachdem Sie (in diesem Fall) die Ausnahme abgefangen haben, dass Ihre Init() method thrrew - Sie MÜSSEN die Ausnahme nicht vom Ctor 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 es nicht der empfohlene Ansatz ist, sondern nur eine zusätzliche Sichtweise teilen wollte.

Wie Sie vielleicht aus einem Teil des Drucks im Code ersehen haben, basiert er auf Punkt 10 im fantastischen "Effektiveren C++" von Scott Meyers (1. Auflage).

Ich hoffe es hilft.

Prost,

Kerl.

3
Guy Avraham