Подтвердить что ты не робот

Исключение исключений в конструкторах С++

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

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

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

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

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

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

4b9b3361

Ответ 1

Как правило, вы получаете такой код для объектов в стеке:

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 вместо метания, чтобы сохранить себе память о том, чтобы использовать newhrow 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. Я не знаю.

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

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

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

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

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

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

Ответ 2

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

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
}

Ответ 3

Что касается ссылки Google (вы не можете найти, как они обрабатывали ошибки в конструкторе):

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

Ответ 4

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

Ответ 5

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

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

Ответ 6

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