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

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

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

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

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

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

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

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
12
0
3 697
6
Перейти к ответу Данный вопрос помечен как решенный

Ответы 6

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

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

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

David Holm 02.12.2008 19:11

Дисциплина написания конструкторов nothrow на самом деле не сильно отличается от дисциплины, требуемой для написания nothrow swap: такой же анализ «доказывает», что он не может бросить. Если ваша функция подкачки должна «обрабатывать ошибки», вы должны перепроектировать свой класс, и то же самое относится и к ctors.

Steve Jessop 02.12.2008 19:31

Тогда просто s / exceptions / errors /. Мысль по-прежнему актуальна: если вы выполняете только тривиальную работу, вы можете быть уверены, что этого не произойдет.

Joel Coehoorn 02.12.2008 19:54

Да, извините, я согласен с тем, что вы говорите, я просто добавляю наблюдение, что «обеспечение отсутствия ошибок для обработки» не совсем чуждо, поскольку это то, что программисты C++ уже делают в свопинге, а вы этого не делаете. не хочу прибегать к выбрасыванию исключений (или их перехвату, в идеале).

Steve Jessop 02.12.2008 21:56

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

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

David Holm 02.12.2008 19:13

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

Вот если бы только объектно-ориентированное приложение могло состоять только из POD-классов :(

David Holm 02.12.2008 19:12

В этом контексте цитирование C++ FQA не имеет смысла. Он был написан с единственной целью убедить людей вообще не использовать C++, и здесь это не вариант.

Nemanja Trifunovic 02.12.2008 19:15

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

coppro 02.12.2008 20:17
Ответ принят как подходящий

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

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 успехов или любое другое значение в случае сбоя.

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

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

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

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

Некоторые части ускорения работают, но у нас уже есть собственный умный указатель, так что это не проблема. Пока мы не рассматриваем std :: bad_alloc как проблему, потому что, если у нас заканчивается память, у нас возникают большие проблемы. Спасибо за все хорошие предложения!

David Holm 02.12.2008 19:36

В этой ситуации использование вложенных if / else для проверки ошибок и использование ключевого слова goto может использоваться для уменьшения глубины вложенности, что упрощает выполнение кода. Итак, я бы сделал «если (что-то не удалось) goto error_handler;»

Skizz 02.12.2008 20:09

Вероятно, стоит отметить, что этот паттерн известен как «двухфазное построение».

ChrisN 02.12.2008 21:33

@ChrisN: да, это пробуждает воспоминания. Я следую принципу, что если термин не появляется в имени класса или функции, я могу его забыть. И если он появляется в имени, я могу найти его в документации, так что я снова могу это забыть ;-)

Steve Jessop 02.12.2008 21:52

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

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 другие функции, например, то, что проверяет значение кода.

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

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
}

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