Вопрос

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

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

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

Это было полезно?

Решение

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

Другие советы

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

#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

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

Можно создавать из вашего конструктора, но вы должны убедиться, что ваш объект создан после Главная начала и до его заканчивается:

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

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

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

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

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

Помимо факт, который вам не нужно выбрасывать из конструктора в вашем конкретном случае, потому что pthread_mutex_lock фактически возвращает ЭЙНВАЛ если ваш мьютекс не был инициализирован и вы можете бросить после звонка в 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) для вашего класса с указателем(ы) члены данных.

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top