Бросать исключения из конструкторов - PullRequest
254 голосов
/ 01 мая 2009

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

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

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

Ответы [ 10 ]

241 голосов
/ 01 мая 2009

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

95 голосов
/ 01 мая 2009

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

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

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

15 голосов
/ 29 июня 2016
#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

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

14 голосов
/ 01 мая 2009

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

Помимо факта, что вам не нужно бросать из конструктора в вашем конкретном случае, потому что 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-acquisition-is-Initialization) программирования парадигма.

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

Все эти формы 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
};
4 голосов
/ 19 декабря 2013

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

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

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

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

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

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

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

3 голосов
/ 10 апреля 2018

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

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

Итак, к делу : В случае, если вы хотите, чтобы Dtor вашего класса "предпринял действие", когда вы вызываете его после (для этого случая), вы поймаете исключение, что ваш 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'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-е издание).

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

Приветствия

Guy.

3 голосов
/ 01 мая 2009

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

0 голосов
/ 01 мая 2009

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

...