Исключение исключений в конструкторах C ++ - PullRequest
12 голосов
/ 02 декабря 2008

Недавно мы столкнулись с проблемой переноса нашей платформы C ++ на платформу ARM под управлением uClinux, где единственным поддерживаемым поставщиком компилятором является GCC 2.95.3. Проблема, с которой мы столкнулись, заключается в том, что исключения крайне ненадежны, вызывая все, от того, что не перехватывается вообще, до перехвата неродственным потоком (!). Это похоже на задокументированную ошибку, то есть здесь и здесь .

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

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

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

Согласно руководству по стилю Google C ++ они не используют исключения и выполняют только тривиальную работу в своих конструкторах, используя метод init для нетривиальной работы ( Doing Work in Constructors ) , Однако я не могу найти ничего о том, как они обрабатывают ошибки конструкции при использовании этого подхода.

Кто-нибудь здесь пытался устранить исключения и придумал хорошее решение для обработки сбоев конструкции?

Ответы [ 6 ]

13 голосов
/ 02 декабря 2008

Обычно вы получаете такой код для объектов в стеке:

MyClassWithNoThrowConstructor foo;
if (foo.init(bar, baz, etc) != 0) {
    // error-handling code
} else {
    // phew, we got away with it. Now for the next object...
}

И это для объектов в куче. Я предполагаю, что вы переопределяете глобальный оператор new чем-то, что возвращает NULL вместо throw, чтобы сэкономить, помня использовать везде nothrow new:

MyClassWithNoThrowConstructor *foo = new MyClassWithNoThrowConstructor();
if (foo == NULL) {
    // out of memory handling code
} else if (foo->init(bar, baz, etc) != 0) {
    delete foo;
    // error-handling code
} else {
    // success, we can use foo
}

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

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

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

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

Возможно, вы сможете быстро взглянуть на некоторые документы по API класса Symbian в Интернете. Symbian использует C ++ без исключений: у него есть механизм, называемый «Leave», который частично компенсирует это, но он не подходит для выхода из конструктора, поэтому у вас есть одна и та же базовая проблема с точки зрения разработки безотказных конструкторов и отсрочки сбоя при сбое. операции для инициации процедур. Конечно, в Symbian подпрограмме init разрешено выходить, так что вызывающей программе не нужен код обработки ошибок, который я указал выше, но с точки зрения разделения работы между конструктором C ++ и дополнительным вызовом init это тоже самое.

Общие принципы включают в себя:

  • Если ваш конструктор хочет получить значение откуда-то таким способом, который может привести к сбою, перенесите его в init и оставьте значение по умолчанию инициализированным в ctor.
  • Если ваш объект содержит указатель, задайте ему значение null в ctor и установите его "правильно" в init.
  • Если ваш объект содержит ссылку, либо измените его на (умный) указатель, чтобы он мог начинаться с нуля, либо заставьте вызывающую функцию передать значение в конструктор в качестве параметра вместо генерации его в ctor.
  • Если в вашем конструкторе есть члены типа объекта, то все в порядке. Их ctors тоже не будут выбрасывать, так что вполне нормально построить своих членов (и базовые классы) в списке инициализаторов обычным способом.
  • Убедитесь, что вы отслеживаете, что установлено, а что нет, чтобы деструктор работал при сбое инициализации.
  • Все функции, кроме конструкторов, деструктора и init, могут предполагать, что init завершился успешно, при условии, что вы документируете для своего класса, что недопустимо вызывать любой метод, кроме init, до тех пор, пока init не будет вызван и завершится успешно.
  • Вы можете предложить несколько функций инициализации, которые в отличие от конструкторов могут вызывать друг друга, так же, как для некоторых классов вы предлагаете несколько конструкторов.
  • Вы не можете предоставить неявные преобразования, которые могут потерпеть неудачу, поэтому, если ваш код в настоящее время полагается на неявные преобразования, которые генерируют исключения, вам придется перепроектировать. То же самое касается большинства перегрузок операторов, поскольку их возвращаемые типы ограничены.
1 голос
/ 02 декабря 2008

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

class MyClass
{
public:
    MyClass() : m_resource(NULL)
    {
        m_resource = GetResource();
    }
    bool IsValid() const
    {
        return m_resource != NULL;
    }
private:
    Resource * m_resource;
};

MyClass myobj;
if (!myobj.IsValid())
{
    // error handling goes here
}
0 голосов
/ 02 декабря 2008

Если вы действительно не можете использовать исключения, вы также можете написать строительный макрос, делающий то, что всегда предлагал кто-то другой. Таким образом, вам не придется постоянно выполнять этот цикл creation / init / if и самое главное, вы никогда не забудете инициализировать объект.

struct error_type {
    explicit error_type(int code):code(code) { }

    operator bool() const {
        return code == 0;
    }

    int get_code() { return code; }
    int const code;
};

#define checked_construction(T, N, A) \
   T N; \
   if(error_type const& error = error_type(N.init A))

Структура error_type инвертирует условие, поэтому ошибки проверяются в else части if. Теперь напишите функцию инициализации, которая возвращает 0 в случае успеха, или любое другое значение, указывающее код ошибки.

struct i_can_fail {
    i_can_fail() {
        // constructor cannot fail
    } 

    int init(std::string p1, bool p2) {
        // init using the given parameters
        return 0; // successful
    } 
};

void do_something() {
    checked_construction(i_can_fail, name, ("hello", true)) {
        // alright. use it
        name.do_other_thing();
    } else {
        // handle failure
        std::cerr << "failure. error: " << error.get_code() << std::endl;
    }

    // name is still in scope. here is the common code
}

Вы можете добавить другие функции к error_type, например, материал, который ищет, что означает код.

0 голосов
/ 02 декабря 2008

Если конструктор выполняет только тривиальные задачи, такие как инициализация переменных POD (и неявный вызов других тривиальных конструкторов), то он не может завершиться ошибкой. См. C ++ FQA ; см. также почему не следует использовать исключения C ++ .

0 голосов
/ 02 декабря 2008

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

0 голосов
/ 02 декабря 2008

Относительно ссылки Google (вы не могли найти, как они обрабатывали ошибки в конструкторе):

Ответ на этот вопрос таков: если они выполняют только тривиальную работу в конструкторе, то ошибок нет. Поскольку работа тривиальна, они довольно уверены (подкреплены тщательным тестированием, я уверен), что исключения просто не будут выброшены.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...