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

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

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

Предположим, что я обматываю мьютекс 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 и вызвать pthread mutex_init в котором будет возвращен bool на основе pthread mutex_init return? Таким образом, мне не нужно использовать исключения для такого объекта с низким уровнем.

Ответ 1

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

Ответ 2

Если вы делаете исключение из конструктора, имейте в виду, что вам нужно использовать синтаксис 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
    { ... }

Ответ 3

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

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

Ответ 4

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

Ответ 5

#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

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

Ответ 6

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

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;

}

Проблема с этим подходом находится на стороне вызывающего абонента. Каждый пользователь класса должен был бы сделать, если до фактического использования объекта. Это призыв к ошибкам - нет ничего проще, чем забыть проверить состояние перед продолжением.

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

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

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

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

С помощью этого решения вы можете получить флаг состояния на месте, как возвращаемое значение метода factory, не введя конструктор с плохими данными.

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

Ответ 7

Помимо факта, который вам не нужно бросать из конструктора в вашем конкретном случае, потому что 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 (Resource-gather-is-Initialization). p >

Отметьте этот пример в 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)
}

Сосредоточьтесь на этих утверждениях:

  • static std::mutex mutex
  • std::lock_guard<std::mutex> lock(mutex);
  • 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) Может быть объявлен static и фактически будет объявлен как переменная-член (2) Никогда не ожидается, что он будет объявлен как переменная-член (3) Ожидается, что он будет объявлен как переменная-член, а основной ресурс может быть недоступен.

Все эти формы RAII; Чтобы решить эту проблему, нужно проанализировать RAII.

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

Это не требует, чтобы вы инициализировали и подключили все к конструкции. Например, если вы создадите объект сетевого клиента, вы бы не подключили его к серверу при создании, поскольку это медленная операция с сбоями. Вместо этого вы должны написать функцию 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
};

Ответ 8

Единственный раз, когда вы НЕ выбрали исключения из конструкторов, - если ваш проект имеет правило против использования исключений (например, Google не любит исключений). В этом случае вы не захотите использовать исключения в своем конструкторе больше, чем где-либо еще, и вместо этого вам придется иметь какой-то метод init.

Ответ 9

Добавляя ко всем ответам здесь, я подумал упомянуть очень конкретную причину/сценарий, где вам может захотеть отбросить исключение из метода Init а не из Ctor (что из-за курса является предпочтительным и более распространенным подходом),

Я заранее упомянул, что в этом примере (сценарии) предполагается, что вы не используете "интеллектуальные указатели" (ie- std::unique_ptr) для членов данных указателя (ов) вашего класса.

Итак, если вы хотите, чтобы Dtor вашего класса "принял действие", когда вы вызываете его после (для этого случая), вы поймаете исключение, которое Init() ваш метод Init() - вы НЕ ДОЛЖНЫ выбросить исключение из 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 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 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 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-е издание).

Надеюсь, поможет.

Ура,

Guy.

Ответ 10

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