В соответствии с ПОЛУЧИЛ № 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 ++ предоставляет компилятору некоторую свободу с порядком вычисления выражений, потому что это позволяет компилятору выполнять оптимизации, которые в противном случае были бы невозможны. Чтобы разрешить это, правила оценки выражений задаются способом, который не является безопасным для исключений, и поэтому, если вы хотите написать безопасный для исключений код, вы должны знать об этих случаях и избегать их. (Смотрите ниже, как лучше всего это сделать.)
Итак, мои вопросы:
Как исправить это типичное исключение небезопасного кода? Должны ли мы просто избегать написания такого кода?
Ответ меня немного смутил, чтобы справиться
сбои конструктора, мы должны выбросить исключение из конструктора в соответствии с C ++ FAQ и убедитесь, что выделенная память освобождена должным образом, поэтому при условии, что класс T реализовал код, который обрабатывает сбой конструкции, есть ли у нас проблема безопасности исключений в приведенном выше коде?
Спасибо за ваше время и помощь.
Сначала напиши 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
с помощью ()
или же {}
, но ничто не идеально.
Как исправить это типичное исключение небезопасного кода?
Используйте умные указатели. Если ваша функция использует голые указатели и вы не можете это контролировать, вы можете сделать это следующим образом:
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)
но это еще один вопрос).
Первое: да, избегайте проблем с безопасностью потоков.
Если вы не можете переписать 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>());