Этот пост - перевод статьи Герба Саттера: GotW #102: Exception-Safe Function Calls.
Есть ли в этом коде проблемы с безопасностью относительно исключений или любые другие проблемы? Объясните.
3. Продолжая изучать старый код, вы обнаруживаете, что кому-то не нравился код, приведенный в примере 2, и позднее он был изменен на:
Какие улучшения привнес новый подход относительно старого (и привнес ли вообще)? Остались ли проблемы с безопасностью относительно исключений? Объясните.
4. Покажите, как можно реализовать функцию make_unique, которая бы решала проблемы с безопасностью в примере 3, и которую можно было бы вызывать следующим образом:
Давайте поймем, используя эти правила, что происходит в наших первых примерах.
В примере 1(а) мы можем сказать, что оба выражения expr1 и expr2 должны быть выполнены до того, как будет вызвана функция f.
И это все. Компилятор может выполнить выражение expr1 как до так и после выражения expr2, более того, составляющие их выражения могут даже чередоваться между собой при выполнении. Находится достаточное число людей, которые находят этот факт удивительным, чтобы этот вопрос постоянно всплывал на форумах, посвященных С++, тогда как это всего лишь прямое следствие правил С и С++ последовательности выполнения выражений внутри одного потока выполнения.
В примере 1(b) функции и выражения могут быть выполнены в любом порядке, который не противоречит следующим правилам:
2. Разбирая доставшийся вам в наследство код, вы наткнулись на следующий фрагмент:
Да, здесь есть несколько потенциальных проблем с безопасностью относительно исключений.
Небольшое повторение: что происходит когда выполняется выражение типа new T? Вспомним, что выражение с new на на самом деле делает (опустим здесь формы опреатора new для выделения массива и Placement new, так как сейчас это не имеет значения):
В случае с примером 2 рассмотрим, что случится, если компилятор решит сгенерировать код, который выполняет эти шаги в следующем порядке:
Есть и другая возможная последовательность шагов:
Следуя духу С в плане эффективности, стандарт С++ оставляет компилятору некоторую свободу в определении порядка выполнения выражений, потому что это позволяет компилятору выполнять оптимизации, которые в противном случае были бы невозможны. Чтобы позвоить проводить оптимизации, правила порядка выполнения выражений сформулированы таким образом, что не защищают от проблем с безопасностью относительно исключений. Поэтому, если вы хотите писать безопасный относительно исключений код, вам следует знать о подобных возможных проблемах и избегать их. К счастью, вы можете писать писать подобный код и при этом избегать уазанных проблем. Возможно, умный указатель типа unique_ptr мог бы помочь?
3. Продолжая изучать старый код, вы обнаруживаете, что кому-то не нравился код, приведенный в примере 2, и позднее он был изменен на:
В этом коде делается попытка победить проблему с помощью unique_ptr. Многие люди считают, что умный указатель - это панацея, когда дело доходит до безопасности относительно исключений, некоторый краеугольный камень или амулет, который одним своим присутствием где-либо поблизости оберегает от беспутсв компилятора.
Это не такю Ничего не изменилось. Код в примере 3 по-прежнему не является безопасным относительно исключений и ровно по тем же причинам, что и раньше.
Конкретнее, проблема заключается в том, что ресурсы защищены, когда они уже находятся под управлением unique_ptr, но уже упомянутые неприятности могут произойти еще до того, как любой из конструкторов unique_ptr будет вызван. Это происходит потому, что операции могут по-прежнему выполнятся в неудачном порядке, как и в примере выше, с той лишь разницей, что теперь к ним перд вызовом f присоединяются вызовы конструкторов unique_ptr. Например:
К счастью, проблема не в самом unique_ptr, а в том, что он используется неправильно.
4. Покажите, как можно реализовать функцию make_unique, которая бы решала проблемы с безопасностью в примере 3, и которую можно было бы вызывать следующим образом:
Основная идея заключается в следующем:
Такой подход решает наши проблемы с безопасностью относительно исключений. Больше нет такой возможной последовательности выполнения отдельных выражений, которая бы привела к утечке ресурсов, потому что сейчас у нас есть только две функции, и мы знаем, что одна из них будет полностью выполнена до того, как другая будет вызвана. Рассмотрим следующий порядок выполнения:
Простой вопрос
1. Что вы можете сказать о порядке выполнения функций f, g и h и выражений expr1 и expr2 в приведенных ниже фрагментах кода? Предпалагается, что expr1 и expr2 не содражат в себе вызовов других функций.1 2 3 | // Пример 1(a) // f( expr1, expr2 ); |
1 2 3 | // Пример 1(b) // f( g( expr1 ), h( expr2 ) ); |
Вопросы посложнее
2. Разбирая доставшийся вам в наследство код, вы наткнулись на следующий фрагмент:1 2 3 4 5 6 7 | // Пример 2 // В заголовочном файле: void f( T1*, T2* ); // В месте вызова: f( new T1, new T2 ); |
3. Продолжая изучать старый код, вы обнаруживаете, что кому-то не нравился код, приведенный в примере 2, и позднее он был изменен на:
1 2 3 4 5 6 7 | // Пример 3 // В заголовочном файле: void f( std::unique_ptr<T1>, std::unique_ptr<T2> ); // В месте вызова: f( std::unique_ptr<T1>{ new T1 }, std::unique_ptr<T2>{ new T2 } ); |
4. Покажите, как можно реализовать функцию make_unique, которая бы решала проблемы с безопасностью в примере 3, и которую можно было бы вызывать следующим образом:
1 2 3 4 5 6 7 | // Пример 4 // В заголовочном файле: void f( std::unique_ptr<T1>, std::unique_ptr<T2> ); // В месте вызова: f( make_unique<T1>(), make_unique<T2>() ); |
Решение
1. Что вы можете сказать о порядке выполнения функций f, g и h и выражений expr1 и expr2 в приведенных ниже фрагментах кода? Предпалагается, что expr1 и expr2 не содеражат в себе вызовов других функций.
Ответ может быть дан исходя из следующих основных правил:- Значения всех аргументов функции должны быть вычислены перед тем, как сама функция будет вызвана. Это означает также полное выполнение всех дополнительных действий, заданных в выражениях, которые выступают в качестве аргумента функции.
- Как только начинается выполнение функции, никакие выражения принадлежащие вызывающей функции не могут начать или продолжить выполняться, пока вызванная функция не завершится. Выполнение инструкций одной функции не может чередоваться с выполнением выражений другой.
- Выражения, которые используются в качестве аргумента функции, в общем случае могут выполняться в любом порядке, в том числе выполнение выражений, из которых состоит одно выражение, может чередоваться с выполнением выражений, из которых состоит другое, если это не противоречит другим правилам.
Давайте поймем, используя эти правила, что происходит в наших первых примерах.
1 2 3 | // Пример 1(a) // f( expr1, expr2 ); |
И это все. Компилятор может выполнить выражение expr1 как до так и после выражения expr2, более того, составляющие их выражения могут даже чередоваться между собой при выполнении. Находится достаточное число людей, которые находят этот факт удивительным, чтобы этот вопрос постоянно всплывал на форумах, посвященных С++, тогда как это всего лишь прямое следствие правил С и С++ последовательности выполнения выражений внутри одного потока выполнения.
1 2 3 | // Пример 1(b) // f( g( expr1 ), h( expr2 ) ); |
- выражение expr1 должно быть выполнено до того, как будет вызвана функция g;
- выражение expr2 должно быть выполнение до того, как будет вызвана функция h;
- функции g и h должны завершиться до того, как будет вызвана функция f.
Немного о проблемах безопасности относительно исключений при вызове функций
2. Разбирая доставшийся вам в наследство код, вы наткнулись на следующий фрагмент:
1234567 // Пример 2
// В заголовочном файле:
void
f( T1*, T2* );
// В месте вызова:
f(
new
T1,
new
T2 );
Есть ли в этом коде проблемы с безопасностью относительно исключений или любые другие проблемы? Объясните.
Да, здесь есть несколько потенциальных проблем с безопасностью относительно исключений.
1 2 3 4 5 6 7 | // Пример 2 // В заголовочном файле: void f( T1*, T2* ); // В месте вызова: f( new T1, new T2 ); |
Небольшое повторение: что происходит когда выполняется выражение типа new T? Вспомним, что выражение с new на на самом деле делает (опустим здесь формы опреатора new для выделения массива и Placement new, так как сейчас это не имеет значения):
- оно выделяет память;
- оно создает объект типа T в выделенной памяти; и
- если создание объекта завершается исключением, оно освобождает выделенную память.
В случае с примером 2 рассмотрим, что случится, если компилятор решит сгенерировать код, который выполняет эти шаги в следующем порядке:
- выделить память для Т1;
- создать Т1;
- выделить память для Т2;
- создать Т2;
- вызвать f.
Есть и другая возможная последовательность шагов:
- выделить память для Т1;
- выделить память для Т2;
- создать Т1;
- создать Т2;
- вызвать f.
- Если исключение будет выброшено на шаге 3, память, выделенная под объект T1 будет автоматически освобождена. Но стандарт не гарантирует, что память, выделенная под объект Т2 тоже будет освобождена. Следовательно, память "утекает".
- Если исключение будет выброшено на шаге 4, то объект Т1 будет к этому времени уже полностью создан, а поэтому никто не гарантирует нам его удаление и освобождение выделенной под него памяти. Следовательно, объект Т1 "утекает".
Следуя духу С в плане эффективности, стандарт С++ оставляет компилятору некоторую свободу в определении порядка выполнения выражений, потому что это позволяет компилятору выполнять оптимизации, которые в противном случае были бы невозможны. Чтобы позвоить проводить оптимизации, правила порядка выполнения выражений сформулированы таким образом, что не защищают от проблем с безопасностью относительно исключений. Поэтому, если вы хотите писать безопасный относительно исключений код, вам следует знать о подобных возможных проблемах и избегать их. К счастью, вы можете писать писать подобный код и при этом избегать уазанных проблем. Возможно, умный указатель типа unique_ptr мог бы помочь?
3. Продолжая изучать старый код, вы обнаруживаете, что кому-то не нравился код, приведенный в примере 2, и позднее он был изменен на:
1234567 // Пример 3
// В заголовочном файле:
void
f( std::unique_ptr<T1>, std::unique_ptr<T2> );
// В месте вызова:
f( std::unique_ptr<T1>{
new
T1 }, std::unique_ptr<T2>{
new
T2 } );
Какие улучшения привнес новый подход относительно старого (и привнес ли вообще)? Остались ли проблемы с безопасностью относительно исключений? Объясните.
В этом коде делается попытка победить проблему с помощью unique_ptr. Многие люди считают, что умный указатель - это панацея, когда дело доходит до безопасности относительно исключений, некоторый краеугольный камень или амулет, который одним своим присутствием где-либо поблизости оберегает от беспутсв компилятора.1 2 3 4 5 6 7 | // Пример 3 // В заголовочном файле: void f( std::unique_ptr<T1>, std::unique_ptr<T2> ); // В месте вызова: f( std::unique_ptr<T1>{ new T1 }, std::unique_ptr<T2>{ new T2 } ); |
Это не такю Ничего не изменилось. Код в примере 3 по-прежнему не является безопасным относительно исключений и ровно по тем же причинам, что и раньше.
Конкретнее, проблема заключается в том, что ресурсы защищены, когда они уже находятся под управлением unique_ptr, но уже упомянутые неприятности могут произойти еще до того, как любой из конструкторов unique_ptr будет вызван. Это происходит потому, что операции могут по-прежнему выполнятся в неудачном порядке, как и в примере выше, с той лишь разницей, что теперь к ним перд вызовом f присоединяются вызовы конструкторов unique_ptr. Например:
- выделить память для Т1;
- создать Т1;
- выделить память для Т2;
- создать Т2;
- создать unique_ptr<T1>;
- создать unique_ptr<T2>;
- вызвать f.
- выделить память для Т1;
- выделить память для Т2;
- создать Т1;
- создать Т2;
- создать unique_ptr<T1>;
- создать unique_ptr<T2>;
- вызвать f.
К счастью, проблема не в самом unique_ptr, а в том, что он используется неправильно.
Представляем make_unique
4. Покажите, как можно реализовать функцию make_unique, которая бы решала проблемы с безопасностью в примере 3, и которую можно было бы вызывать следующим образом:
1234567 // Пример 4
// В заголовочном файле:
void
f( std::unique_ptr<T1>, std::unique_ptr<T2> );
// В месте вызова:
f( make_unique<T1>(), make_unique<T2>() );
Основная идея заключается в следующем:1 2 3 4 5 6 7 | // Пример 4 // В заголовочном файле: void f( std::unique_ptr<T1>, std::unique_ptr<T2> ); // В месте вызова: f( make_unique<T1>(), make_unique<T2>() ); |
- мы хотим использовать тот факт, что инструкции из разных функций, вызываемых из одного потока, не чередуются между собой при выполнении. Поэтому мы хотим предоставить функцию, которая бы выполняла выделение памяти под объект, создание объекта, и создание unique_ptr.
- Так как функция должна быть в состоянии работать с любыми типами, мы хотим реализовать ее в виде шаблонной функции.
- Так как вызывающий код должен иметь возможность передавать параметры конструктора объекта в make_unique, мы используем превосходный стиль пересылки параметров в С++11, чтобы передать параметры в выражение new.
- Так как для shared_ptr уже имеется аналогичная функция make_shared, мы назовем нашу функцию make_unique. Тот факт, что в С++11 нет make_unique частично может быть объяснен недосмотром (он исправлен в С++14). Если у вас нет возможности использовать С++14 - используйте представленную реализацию.
1 2 3 4 5 | template < typename T, typename ...Args> std::unique_ptr<T> make_unique( Args&& ...args ) { return std::unique_ptr<T>( new T( std::forward<Args>(args)... ) ); } |
- вызвать make_unique<T1>;
- вызвать make_unique<T2>;
- вызвать f.
Временные объекты уничтожаются на последнем шаге выполнения выражения которое (лексически) включает в себя их создание. Это верно даже если выражение выбрасывает исключение.
Рекомендации:
- Предпочитайте создавать объекты, которые будут управляться shared_ptr, с помощью make_shared, а объекты, которые будут управляться unique_ptr, с помощью make_unique.
- Хотя стандарт С++11 не предоставляет make_unique, это по большей части сделано по недосмотру (и исправлено в стандарте С++14). Если вы не можете использовать С++14, используйте реализацию make_unique, представленную выше.
- Избегайте использования чистых вызовов оператора new, или других незащищенных способов выделения памяти. Вместо это используйте фабрику типа make_unique, которая является оберткой над чистым выделеним памяти и немедленно передает выделенную память в другой объект, который и будет этой памятью управлять. Часто этим объектом будет умный указатель, но это также может быть любой тип владеющего объекта, чей деструктор безопасно освободит ресурс.
Комментариев нет :
Отправить комментарий