Как исправить это типичное исключение небезопасного кода?

Согласно GOTW # 56, в следующем коде есть потенциальная классическая утечка памяти и исключительные проблемы безопасности.

//  In some header file:
void f( T1*, T2* );
//  In some implementation file:
f( new T1, new T2 );

Причина в том, что когда мы new T1, или же new T2 могут быть исключения из конструкторов классов.

Между тем, согласно объяснениям:

Краткое резюме: Выражение типа "new T1" называется, достаточно просто, новым выражением. Вспомните, что на самом деле делает new-выражение (для простоты я буду игнорировать формы размещения и массива, поскольку они здесь не очень актуальны):

  • это выделяет память

  • он создает новый объект в этой памяти

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

Таким образом, каждое новое выражение по сути представляет собой последовательность из двух вызовов функций: один вызов оператора new() (либо глобального, либо один, предоставляемый типом создаваемого объекта), а затем вызов конструктора.

Для примера 1 рассмотрим, что произойдет, если компилятор решит сгенерировать код следующим образом:

1: выделить память для T1
2: построить Т1
3: выделить память для T2
4: построить Т2
5: вызов f()

Проблема заключается в следующем: если шаг 3 или шаг 4 не выполняются из-за исключения, стандарт C++ не требует уничтожения объекта T1 и освобождения его памяти. Это классическая утечка памяти, и явно не в добре. [...]

Читая больше:

Почему стандарт просто не предотвращает проблему, требуя от компиляторов правильных действий, когда дело доходит до очистки?

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

Итак, мои вопросы:

  1. Как исправить это типичное исключение небезопасного кода? Должны ли мы просто избегать написания такого кода?

  2. Ответ немного смутил меня, чтобы справиться с ошибками конструктора, мы должны выбросить исключение из конструктора в соответствии с C++ FAQ и убедиться, что выделенная память освобождена должным образом, поэтому, предполагая, что класс T реализовал код, который обрабатывает ошибки конструирования, все еще есть проблема безопасности исключений в приведенном выше коде?

Спасибо за ваше время и помощь.

3 ответа

Решение

Сначала напиши make_unique:

template<typename T, typename... Args>
std::unique_ptr<T> make_unique( Args&&... args ) {
  return {new T(std::forward<Args>(args)...)};
}

который не был включен в стандарт, в основном как недосмотр. std::make_shared есть и make_unique вероятно появится в C++14 или 17.

Во-вторых, измените подпись вашей функции на:

//  In some header file:
void f( std::unique_ptr<T1>, std::unique_ptr<T2> );

и назовите это как:

f( make_unique<T1>(), make_unique<T2>() );

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

Если T1 имеет нетривиальные конструкторы, которые вы хотите использовать, вы можете просто передать аргументы make_unique<T1> и он отлично перенаправляет их в конструктор T1,

Есть проблемы с несколькими способами построения T1 с помощью () или же {}, но ничто не идеально.

Первое: да, избегайте проблем с безопасностью потоков.

Если вы не можете переписать f, учти это:

auto t1 = std::make_unique<T1>(); //C++14
std::unique_ptr<T2> t2{new T2}; //C++11
f( t1.get(), t2.get() );        //or release(), depending on ownership policies of f

Если вы можете, однако, сделать это:

void f(std::unique_ptr<T1>, std::unique_ptr<T2>);

//call:
f(make_unique<T1>(), make_unique<T2>());

Как исправить это типичное исключение небезопасного кода?

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

std::unique_ptr<T1> t1(new T1);
std::unique_ptr<T2> t2(new T2);
// assuming f takes ownership of the pointers:
f(t1.release(), t2.release());

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

Здесь безопасность исключений имеет мало общего с тем, могут ли конструкторы генерировать или нет: new сам мог бросить, потому что не мог выделить память, и мы вернулись к исходной точке. Так что при выделении объекта с newвсегда следует предполагать, что конструкция может потерпеть неудачу, даже если конструктор явно заявляет, что не может выдать ее (кроме, конечно, при использовании new(std::nothrow) но это уже другое дело).

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