Выбрасывание исключений из конструкторов

у меня дискуссия с коллегой о бросании исключений из конструкторов, и я подумал, что хотел бы получить некоторую обратную связь.

можно ли создавать исключения из конструкторов с точки зрения дизайна?

допустим, я обертываю мьютекс 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 сбой вызова объект мьютекса непригоден для использования, поэтому возникает исключение гарантирует, что мьютекс не будет создан.

должен ли я скорее создать функцию-член init для класса мьютекса и вызвать pthread mutex_init В которая возвращает bool на основе pthread mutex_init - вернуться? Таким образом, мне не нужно использовать исключения для такого объекта низкого уровня.

10 ответов


Да, создание исключения из неудачного конструктора является стандартным способом сделать это. Прочитайте этот FAQ о обработка конструктора, который терпит неудачу для получения дополнительной информации. Наличие метода init() также будет работать, но каждый, кто создает объект мьютекса, должен помнить, что init () должен быть вызван. Я чувствую, что это идет против RAII принципе.


Если вы создаете исключение из конструктора, имейте в виду, что вам нужно использовать синтаксис функции try/catch, если вам нужно поймать это исключение в списке инициализаторов конструктора.

например

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

и

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

выбрасывание исключения-лучший способ справиться с ошибкой конструктора. В частности, вы должны избегать половины построения объекта, а затем полагаться на пользователей вашего класса для обнаружения сбоя конструкции путем тестирования переменных флага.

в связи с этим тот факт, что у вас есть несколько разных типов исключений для работы с ошибками мьютекса, меня немного беспокоит. Наследование-отличный инструмент,но его можно использовать чрезмерно. В этом случае я, вероятно, предпочел бы один Исключение MutexError, возможно содержащее информативное сообщение об ошибке.


это нормально, чтобы бросить из вашего конструктора, но вы должны убедиться, что ваш объект построен после 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)
  {
  }
}

#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

деструкторы не вызываются, поэтому, если в конструкторе нужно выбросить исключение, много вещей (например, очистить?) делать.


Если ваш проект обычно полагается на исключения, чтобы отличить плохие данные от хороших данных, то создание исключения из конструктора лучше, чем не бросать. Если исключение не выбрасывается, то объект инициализируется в состоянии зомби. Такой объект должен предоставить флаг, который говорит, является ли объект правильным или нет. Что-то вроде этого:--3-->

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% работоспособен от простого факта, что они его получили.

это обсуждение может продолжаться во многих направление.

например, использование исключений в качестве проверки является плохой практикой. Один из способов сделать это-попробовать шаблон в сочетании с классом factory. Если вы уже используете фабрики, то напишите два метода:

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

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) ofstream кажется, нет RAII, так как состояние объектов должно быть проверено путем вызова is_open() проверки failbit флаг.

на первый взгляд кажется, что он не определился, на чем онстандартным способом в первом случае std::mutex не бросает инициализацию, * в отличие от реализации OP * . Во втором случае он будет выбрасывать все, что выбрасывается из std::mutex::lock, а в третьем нет броска вообще.

обратите внимание на различия:

(1) может будет объявлен статическим и фактически будет объявлен как переменная-член (2) фактически никогда не будет объявлено как переменная-член (3) как ожидается, будет объявлен как переменная-член, и базовый ресурс не всегда может быть доступен.

все эти формы РАИИ; чтобы разрешить это, нужно проанализировать РАИИ.

  • ресурс : объект
  • приобретение (распределение): вы возражаете быть создано
  • инициализации : ваш объект находится в его инвариантное состояние

это не требует от вас инициализации и подключения всего на строительстве. Например, когда вы создаете объект сетевого клиента, вы фактически не подключаете его к серверу при создании, так как это медленная операция с сбоями. Вместо этого вы напишете connect функции для этого. С другой стороны, вы можете создать буферы или просто установить их государство.

таким образом, ваш вопрос сводится к определению вашего начального состояния. Если в вашем случае ваше начальное состояние мьютекс должен быть инициализирован тогда вы должны бросить из конструктора. Напротив, это просто прекрасно, чтобы не инициализировать тогда ( как это делается в std::mutex), и определите свое инвариантное состояние как мьютекс создан . Во всяком случае, инвариант не обязательно компрометируется состоянием его объекта-члена, так как mutex_ объект мутирует между locked и unlocked до Mutex методы Mutex::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) для вашего класса с указателем(ы) данных.

Итак, к делу: In case, вы хотите, чтобы Dtor вашего класса "предпринял действие", когда вы вызываете его после (для этого случая), вы поймаете исключение, которое ваш Init() метод throw - вы не должны бросать исключение из Ctor, потому что вызов Dtor для Ctor не вызывается на "наполовину испеченных" объектах.

ниже пример, чтобы продемонстрировать мою точку зрения:

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

Я еще раз упомяну, что это не рекомендуемый подход, просто хотел поделиться дополнительным моментом вид.

кроме того, как вы могли видеть из некоторых печатей в коде - он основан на пункте 10 в фантастическом "более эффективном c++" Скотта Мейерса (1-е издание).

надеюсь, что это помогает.

спасибо,

парень.


хотя я не работал на C++ на профессиональном уровне, на мой взгляд, нормально выбрасывать исключения из конструкторов. Я делаю это (если нужно).Сеть. Проверьте этой и этой ссылка. Это может вас заинтересовать.