Frage

Ich habe eine Debatte mit einem Kollegen über Ausnahmen von Konstrukteuren zu werfen, und dachte, ich würde ein Feedback mögen.

Ist es OK, Ausnahmen von Konstrukteuren zu werfen, von einer Design-Sicht?

Lets sagen, ich bin in einer Klasse eine POSIX Mutex Einwickeln, es wie folgt aussehen würde:

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, dann ist dies der normale Weg, es zu tun? Denn wenn der pthread mutex_init Aufruf fehlschlägt das Mutex-Objekt unbrauchbar ist so eine Ausnahme zu werfen stellt sicher, dass der Mutex wird nicht erstellt werden.

Soll ich eher eine Member-Funktion init für die Mutex-Klasse und Call pthread mutex_init in dem ein Bool auf pthread mutex_init Rückkehr Basis zurückkehren würde? Auf diese Weise muss ich Ausnahmen nicht Objekt für einen so niedrigen Pegel verwenden.

War es hilfreich?

Lösung

Ja, eine Ausnahme von dem ausgefallenen Konstruktor werfen ist der normale Weg, dies zu tun. Lesen Sie diese FAQ zu einen Konstruktor Handhabung, die für weitere Informationen nicht . eine init () Methode hat, wird auch funktionieren, aber alle, die das Objekt der Mutex erzeugt haben, dass die init erinnern () aufgerufen werden. Ich glaube, es geht gegen die RAII Prinzip.

Andere Tipps

Wenn Sie das tun, eine Ausnahme von einem Konstruktor werfen, denken Sie daran, dass Sie die Funktion try / catch-Syntax verwenden, wenn Sie diese Ausnahme in einem Konstruktor Initialisiererliste fangen müssen.

z.

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

eine Ausnahme zu werfen ist der beste Weg, mit Konstruktor Scheitern umzugehen. Sie sollen vor allem vermeiden, dass ein Objekt Halb konstruieren und dann auf Benutzer Ihrer Klasse unter Berufung von Variablen von einer Art Prüfung Flag Konstruktion Fehler zu erfassen.

Auf einem verwandten Punkt ist die Tatsache, dass man mehrere verschiedene Ausnahmetypen für den Umgang mit Mutex Fehler haben beunruhigt mich ein wenig. Vererbung ist ein großes Werkzeug, aber es kann über verwendet werden. In diesem Fall würde ich wahrscheinlich eine einzige MutexError Ausnahme bevorzugen, möglicherweise eine informative Fehlermeldung enthält.

#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 nicht aufgerufen werden, so dass, wenn eine Ausnahme in einem Konstruktor geworfen werden muß, eine Menge Sachen (z. Aufzuräumen?) Zu tun.

Es ist OK von Ihrem Konstruktor zu werfen, aber Sie sollten sicherstellen, dass Ihre Aufgabe wird nach konstruiert Haupt gestartet und bevor es Ausführungen:

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

Wenn Ihr Projekt auf Ausnahmen beruht im Allgemeinen schlechte Daten von guten Daten zu unterscheiden, dann eine Ausnahme zu werfen aus dem Konstruktor bessere Lösung als nicht zu werfen ist. Wenn keine Ausnahme ausgelöst wird, wird Objekt in einem Zombie-Zustand initialisiert. Ein solches Objekt braucht eine Fahne zu belichten, die besagt, ob das Objekt korrekt ist oder nicht. So etwas wie folgt aus:

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 hätte eine tun, wenn vor dem Objekt tatsächlich verwenden. Dies ist ein Aufruf für Fehler -. Es ist nichts einfacher, als zu vergessen, um eine Bedingung zu testen, bevor Sie fortfahren

Bei einer Ausnahme von dem Konstruktor werfen, Einheit, die das Objekt konstruiert soll sofort von Problemen kümmern. Object Verbraucher den Strom hinab sind frei davon ausgehen, dass das Objekt 100% Betriebs aus der bloßen Tatsache ist, dass sie es erhalten.

Die Diskussion in viele Richtungen weitergehen kann.

Zum Beispiel Ausnahmen als eine Frage der Validierung verwendet, ist eine schlechte Praxis. Eine Möglichkeit, es zu tun, ist ein Try-Muster in Verbindung mit Factory-Klasse. Wenn Sie bereits Fabriken verwenden, dann schreiben zwei Methoden:

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

Mit dieser Lösung können Sie das Status-Flag an Ort und Stelle erhalten, als Rückgabewert der Factory-Methode, ohne jemals den Konstruktor mit schlechten Daten eingeben.

Die zweite Sache ist, wenn Sie den Code mit automatisierten Tests decken. In diesem Fall der Code jedes Stück, das Objekt verwendet, die Ausnahmen nicht werfen würde mit einem zusätzlichen Test abgedeckt werden - ob es richtig wirkt, wenn IsValid () Methode false zurückgibt. Dies erklärt ganz gut, dass Objekte in Zombie-Zustand initialisiert ist eine schlechte Idee.

Neben der Tatsache, dass Sie nicht werfen müssen von dem Konstruktor in Ihrem speziellen Fall, weil pthread_mutex_lock tatsächlich gibt ein EINVAL , wenn Ihr Mutex wurde nicht initialisiert und Sie können den Anruf werfen nach dem lock wie in std::mutex getan wird:

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

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

dann im allgemeinen von Konstrukteuren zu werfen ist ok Erwerb Fehler bei der Konstruktion, und in Übereinstimmung mit RAH (Resource-Acquisition-is- Initialisierung) Programmierparadigma.

Überprüfen Sie diese Beispiel auf 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)
}

Fokus auf diese Aussagen:

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

Die erste Aussage ist RAII und noexcept. In (2) ist es klar, dass RAII auf lock_guard angewendet wird, und es kann tatsächlich throw, während in (3) ofstream nicht scheint RAII zu sein, da die Objekte Zustand durch den Aufruf is_open() zu prüfen hat, der die failbit-Flag überprüft.

Auf dem ersten Blick scheint es, dass es unentschieden ist, was es die Standardmethode und im ersten Fall std::mutex wirft nicht in der Initialisierung * im Gegensatz zu OP Implementierung *. Im zweiten Fall wird es werfen, was von std::mutex::lock geworfen wird, und in den dritten gibt es überhaupt keinen Wurf.

die Unterschiede Hinweis:

(1) Kann statisch deklariert werden und wird tatsächlich als Mitglied Variable deklariert werden (2) Wird eigentlich nie als Mitglied Variable deklariert werden zu erwarten (3) Es wird erwartet, als Mitglied Variable deklariert werden, und die zugrundeliegende Ressource möglicherweise nicht immer verfügbar sein.

Alle diese Formen sind RAH ; dieses Problem zu lösen, muss man RAH analysieren.

  • Resource: Ihr Objekt
  • Acquisition (Allokation): Sie erstellten Objekt, das
  • Initialisierung: Ihre Aufgabe ist es in seinem invariant Zustand

Das ist es nicht erforderlich initialisieren und alles auf dem Bau zu verbinden. Zum Beispiel, wenn würden Sie ein Netzwerk-Client-Objekt erstellen Sie es eigentlich nicht an den Server bei der Erstellung in Verbindung bringen würden, da es ein langsamer Vorgang mit einem Defekt ist. Sie würden stattdessen eine connect Funktion schreiben, genau das zu tun. Auf der anderen Seite könnte man die Puffer schaffen oder einfach nur seinen Zustand gesetzt.

Deshalb kocht Ihr Problem der Definition Ihrer Anfangszustand nach unten. Wenn in Ihrem Fall Ihr Ausgangszustand ist Mutex initialisiert werden muss , dann sollten Sie aus dem Konstruktor werfen. Im Gegensatz ist es einfach gut zu initialisieren nicht dann (wie in std::mutex getan), und definieren Sie Ihren invariant Staat als Mutex erstellt . Auf jeden Fall ist die Invariante nicht notwendigerweise durch den Zustand seines Mitgliedsobjekts, da das mutex_ Objekt mutiert zwischen locked und unlocked durch die Mutex öffentliche Methoden Mutex::lock() und Mutex::unlock() compromized.

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

Das einzige Mal, wenn Sie keine Ausnahmen von Konstrukteuren werfen würde, ist, wenn Ihr Projekt eine Regel gegen die Verwendung von Ausnahmen hat (zum Beispiel, Google nicht mag Ausnahmen). In diesem Fall würden Sie nicht Ausnahmen wollen in Ihrem Konstruktor als irgendwo sonst mehr verwenden, und Sie würden eine Init-Methode von einer Art haben müssen, statt.

Zusätzlich zu all den Antworten hier, dachte ich zu erwähnen, ein ganz bestimmten Grund / Szenario, in dem Sie mögen vielleicht lieber die Ausnahme von der Klasse der Init Methode zu werfen und nicht aus dem Ctor (die vom Kurs abgekommen ist die bevorzugte und mehr gemeinsamer Ansatz).

Ich werde im Voraus erwähnen, dass dieses Beispiel (Szenario) geht davon aus, dass Sie nicht „Smart Pointer“ verwenden (i.e.- std::unique_ptr) für die Klasse s Zeiger (n) Datenelemente.

So auf den Punkt : Falls mögen Sie, dass der Dtor Ihrer Klasse wird „Maßnahmen ergreifen“, wenn Sie es nach aufrufen (für diesen Fall) Sie die Ausnahme abfangen, die Ihre Init() Methode warf - Sie dürfen nicht die Ausnahme von der Ctor werfen, verursachen ein Dtor Aufruf für Ctor ist nICHT auf „unausgegoren“ Objekte aufgerufen.

Siehe unten stehendes Beispiel meines Punkt zeigen:

#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 wieder erwähnen, dass es nicht die empfohlene Vorgehensweise, nur eine zusätzliche Sicht teilen wollte.

Auch, wie Sie von einigen der Druck in dem Code gesehen haben könnte - es basiert auf Artikel 10 in der fantastischen „Effektiver C ++“ von Scott Meyers (1. Auflage)

.

Hoffe, es hilft.

Cheers,

Guy.

Obwohl ich C nicht gearbeitet habe ++ auf einem professionellen Niveau, meiner Meinung nach, ist es in Ordnung, Ausnahmen von den Konstrukteuren zu werfen. Ich tun, dass (bei Bedarf) in .Net. Schauen Sie sich dieser und dieser Link. Es könnte von Ihrem Interesse sein.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top