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

В соответствии с ПОЛУЧИЛ № 56, в следующем коде есть потенциальная классическая утечка памяти и проблемы безопасности исключений:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1

Решение

Сначала напиши 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 с помощью () или же {}, но ничто не идеально.

3

Другие решения

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

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

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) но это еще один вопрос).

2

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

Если вы не можете переписать 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>());
2
По вопросам рекламы [email protected]