Страницы

Поиск по вопросам

Показаны сообщения с ярлыком деструктор. Показать все сообщения
Показаны сообщения с ярлыком деструктор. Показать все сообщения

среда, 29 января 2020 г.

Почему порядок уничтожения данных объекта должен быть обратным к порядку их конструирования?

#cpp #ооп #конструктор #деструктор


Заинтересовал вопрос: а почему порядок уничтожения данных объекта должен быть обратным
к порядку их конструирования? Нельзя ли удалять содержимое объекта в другой последовательности,
отличной от порядка их конструирования? Не связано ли это как-то с выравниванием данных
или с хранением ссылок на функции-члены (поправьте, если выражаюсь некорректно)? Если
есть возможность, предоставьте ссылку на источник. Благодарю!
    


Ответы

Ответ 1



Порядок удаления обратен порядку конструирования по вполне естественным причинам: в процессе конструирования и/или удаления между разными объектами могут существовать взаимозависимости. Если объект А во время своего конструирования полагался на то, что объект Б уже полностью сконструирован, то в процессе деструкции объекта A желательно было бы гарантировать объекту А, что объект Б ещё не деструктирован. Из этого естественным образом вытекает обратный порядок удаления. В случае, когда речь идет о подобъектах объекта класс-типа такая гарантия выглядит более чем естественно: в теле конструктора/деструктора классу, разумеется, нужно, чтобы его подобъекты были уже/еще работоспособны. В более общем случае (например, взаимный порядок создания/удаления разных статических объектов) аналогичные зависимости между разными объектами могут формироваться во время выполнения, то есть никаких шансов предсказать их наличие или доказать их отсутствие у компилятора нет. Поэтому разумным решением видится гарантировать глобальную упорядоченность настолько, насколько это возможно.

Ответ 2



В плюсах есть такое понятие как исключение. При не плановом возврате из функций остаётся не разобранный стек объектов. При очистке стека нужно вызывать деструкторы локальных переменных. Эти деструкторы будут вызываться по порядку декларации в программе и исключение не имеет информации удалён ли объект до этого. По-этому чтобы всё правильно очистилось пишется данная рекомендация. Стандарт : As control passes from a throw-expression to a handler, destructors are invoked for all automatic objects constructed since the try block was entered. The automatic objects are destroyed in the reverse order of the completion of their construction. An object of any storage duration whose initialization or destruction is terminated by an exception will have destructors executed for all of its fully constructed subobjects (excluding the variant members of a union-like class), that is, for subobjects for which the principal constructor (12.6.2) has completed execution and the destructor has not yet begun execution. Similarly, if the non-delegating constructor for an object has completed execution and a delegating constructor for that object exits with an exception, the object’s destructor will be invoked. If the object was allocated in a new-expression, the matching deallocation function (3.7.4.2, 5.3.4, 12.5), if any, is called to free the storage occupied by the object. Перевод: По мере того, как управление переходит от throw-выражения к обработчику, деструкторы вызываются для всех автоматических объектов, созданных с момента ввода блока try. Автоматические объекты уничтожаются в порядке, обратном завершению их строительства. У объекта любой продолжительности хранения, инициализация или уничтожение которого завершается исключением, будут деструкторы, выполняемые для всех его полностью построенных подобъектов (исключая альтернативные члены объединяющего класса), то есть для подобъектов, для которых главный конструктор ( 12.6.2) завершил выполнение, а деструктор еще не начал выполнение. Точно так же, если не делегирующий конструктор для объекта завершил выполнение и делегирующий конструктор для этого объекта завершается с исключением, будет вызван деструктор объекта. Если объект был выделен в новом выражении, соответствующая функция освобождения (3.7.4.2, 5.3.4, 12.5), если таковая имеется, вызывается для освобождения памяти, занятой объектом.

Ответ 3



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

вторник, 28 января 2020 г.

Как перехватить исключение

#cpp #исключения #деструктор


Почему данный код не ловит исключение?

#include 

class A
{
private:

public:
    A()
    {
        std::cout << "A::A()" << std::endl;
    }
    ~A()
    {
        throw 1;
        std::cout << "A::~A(int)" << std::endl;
    }
};

int main()
{
    A* p = nullptr;

    try
    {
        p = new A();
        delete p;
    }
    catch(...)
    {
        std::cout << "catch(...)" << std::endl;
    }

    return 0;
}

    


Ответы

Ответ 1



Выбрасывать исключение из деструктора не рекомендуется и на это есть ряд причин. это может привести к утечкам памяти, невозможно обеспечить какие-либо гарантии безопасности, исключение из деструктора может вылететь во время раскрутки стека, что приведет к разным последствия в зависимости от объекта. Посмотрим на Ваш код. delete p; Как известно, сначала delete-expression вызывает деструктор и затем освобождает память. Если деструктор выбрасывает исключение, то до освобождения памяти дело уже не дойдет и это приведет к утечке. При реализации, например, контейнеров, невозможно обеспечить какие-либо гарантии относительно безопасности исключений. Например: vector.push_back();//Вектор был не пуст Допустим, произошло выделение памяти. Значит старые элементы копируются в новую область памяти. Если копирование прошло успешно, а при уничтожении старых объектов вылетело исключение, то мы получим часть неуничтоженных объектов и утечку памяти. Если же при копировании вылетело исключение и вектор попытается уничтожить уже сконструированные объекты, а при их уничтожении вылетит исключение, то получим ту же утечку, плюс дальше "полетит" уже другое исключение. Если исключение вылетает из деструктора автоматического объекта во время раскрутки стека, то, в соответствии со стандартом, это приводит к вызову std::terminate 15.2 Constructors and destructors The process of calling destructors for automatic objects constructed on the path from a try block to a throw-expression is called “stack unwinding.” [Note: If a destructor called during stack unwinding exits with an exception, terminate is called (15.5.1). So destructors should generally catch exceptions and not let them propagate out of the destructor. —end note] Это цитата из C++03, но в последующих стандартах поведение не поменялось. Начиная с C++11 все деструкторы по-умолчанию имеют спецификацию исключений noexcept. При нарушении этой спецификации вызывается std::terminate: Whenever an exception is thrown and the search for a handler (15.3) encounters the outermost block of a function with an exception-specification that does not allow the exception, then, — if the exception-specification is a dynamic-exception-specification, the function std::unexpected() is called (15.5.2), — otherwise, the function std::terminate() is called (15.5.1). То есть, начиная с C++11 любое исключение, покинувшее деструктор, приводит к вызову std::terminate и аварийному завершению программы, тем самым, как минимум, обеспечивая единое поведение и возможность предоставления каких-либо гарантий. Деструктору возможно указать другую спецификацию исключений: ~A() noexcept(false) { /*...*/ } Но при этом, если вылетит исключение, мы получим все проблемы указанные выше. Итого: исключение, покинувшее деструктор создает много проблем, и не дает никакой пользы, поэтому общая рекомендация - никогда не позволять исключениям покидать деструктор.

Ответ 2



Вообще-то в деструкторах генерировать исключения категорически не рекомендуется. Как я понимаю (на конкретное место в стандарте не укажу) - фактически деструкторы объявлены как noexcept - а значит, при генерации в них исключения будет вызван terminate. Вот, выполните этот код: #include #include class A { private: public: A() { std::cout << "A::A()" << std::endl; } ~A() { throw 1; std::cout << "A::~A(int)" << std::endl; } }; int main() { std::set_terminate([](){ std::cout << "terminate called\n"; exit(1);}); A* p = nullptr; try { p = new A(); delete p; } catch(...) { std::cout << "catch(...)" << std::endl; } return 0; } Как видите, вызывается terminate - как результат генерации исключения там, где его быть не должно. "По-моему, так" (с) Пух

суббота, 4 января 2020 г.

Деструктор производного класса

#cpp #language_lawyer #деструктор


Следует ли объявлять деструктор производного класса виртуальным, если в базовом классе
он уже помечен таковым? Т.е., необходимость в виртуальном деструкторе в базовом классе
мне ясна, в производном - нет. Возможно, не задумывался бы об этом, если бы не натыкался
на статьи, в которых даны примеры, где по мнению авторов наличие виртуальных деструкторов
в производных классах является, видимо, хорошим тоном.
    


Ответы

Ответ 1



Согласно стандарту C++ (7.1.2 Function specifiers) 5 The virtual specifier shall be used only in the initial declaration of a non-static class member function; То есть спецификатор функции virtual обязан присутствовать только в первоначальном объявлении функции. Тем не менее я соглашусь, что присутствие этого спецификатора в объявлениях функций в производных классах делает код более ясным и самодокументируемым. Что касается деструкторов, то, опять-таки, согласно стандарта C++ (10.3 Virtual functions) 6 Even though destructors are not inherited, a destructor in a derived class overrides a base class destructor declared virtual; see 12.4 and 12.5. То есть если деструктор в базовом классе объявлен со спецификатором virtual, то деструктор в производном классе переопределяет деструктор базового класса, то есть ведет себя как виртуальная функция.

Ответ 2



Он автоматически будет виртуальным. Достаточно пометить функцию-член как виртуальную в базовом классе; такой она будет и в производном. #include using namespace std; class B { public: virtual void f() const { cout << "B\n"; } virtual ~B() { cout << "~B\n"; } }; class C: public B { public: void f() const { cout << "C\n"; } ~C() { cout << "~C\n"; } }; class D: public C { public: void f() const { cout << "D\n"; } ~D() { cout << "~D\n"; } }; int main() { B*b = new D; b->f(); delete b; } Хотя D::f() не имеет слова virtual, это ничего не меняет... Код тут: http://ideone.com/mqG4NW

Ответ 3



Как и все остальные переопределенные виртуальные функции, его надо помечать как override, virtual при этом писать не нужно. struct Derived : Base { ~Derived() override; };

четверг, 2 января 2020 г.

Как сообщить вызывающей стороне об ошибке в деструкторе

#cpp #исключения #деструктор


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

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

Например, не удается закрыть файл (или группу файлов), и простое игнорирование этой
ситуации нарушит согласованность огромной базы данных. 

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

Как решается эта проблема?
    


Ответы

Ответ 1



Ответ на этот вопрос есть в C++ super-FAQ. Приведу основные выкладки оттуда и добавлю кое-что от себя. Если в деструкторе произошла аварийная ситуация, можно делать всё что угодно (писать в лог, завершить процесс...), но ни в коем случае не выкидывать исключений наружу деструктора. В противном случае процесс всё равно будет завершён через std::terminate, если во время разворачивания стека из-за одного исключения будет вызвано (и не обработано) новое. В современном C++ любой деструктор уже неявно помечается noexcept, а это значит, что попытка выкинуть из него исключение приведёт к завершению процесса через std::terminate, даже если вызывающий код имеет подходящий try-catch. Чтобы тем не менее иметь возможность выкинуть исключение из деструктора, нужно явно указать noexcept(false): struct S { ~S() noexcept(false) { throw 42; } }; Если иметь возможность проверять наличие исключения (как предложил @Ant в комментарии это можно сделать путём сравнения значений, возвращаемых std::uncaught_exceptions (с++17) в конструкторе и деструкторе объекта), то можно безопасно выбрасывать исключение из деструктора пока ещё нет другого активного исключения, не имеющего своего обработчика. Но в таком случае нужно всё равно как-то обработать ситуацию, если ошибка в деструкторе возникла при наличии такого исключения. Поэтому наиболее простой вариант - просто не выкидывать исключения, а сразу делать что-то другое. Нормальная практика - помещать операции, приводящие к исключениям, в отдельные функции и вызывать их отдельно от деструктора. Код в деструкторе должен быть максимально простым и не требовать сложной обработки, которая может породить исключение. Например, если вы работаете с объектом типа std::fstream, который закрывает файл в деструкторе, но при этом хотите иметь какую-то обработку проблемы закрытия файла, то вызовите std::fstream::close отдельно и проверьте наличие failbit для выявления проблем. Обработайте это как душе угодно (с исключениями или без), а на последующий вызов деструктора std::fstream вам уже не нужно обращать никакого внимания. Здесь стоит заметить, что у std::fstream (через базовый класс std::basic_ios) есть возможность установить битовую маску для генерации исключений с помощью функции exceptions. Если в этой маске будет присутствовать failbit, то деструктор может выкинуть исключение при ошибке закрытия файла (и это приведёт к завершению программы), но если это закрытие, т.е. close(), вызвать самостоятельно, то исключение вполне можно обработать стандартным способом.

Ответ 2



Допустим ошибка в деструкторе является причиной неуспешного завершения программы. И если я заранее имею сомнения по этому поводу, то могу в конструкторе изменить функциональность завершения. Например: struct A { std::terminate_handler th; int i = 5; A() { th = std::set_terminate([](){ std::cerr << "error in class destructor"; }); } ~A() { if (i != 5) std::terminate(); std::set_terminate(th);} }; И программа: A a; a.i = 0; Выдаст сообшение перед завершением. Вопрос не включает в себя конкретного примера, поэтому трудно угадать что вам нужно конкретно...

суббота, 21 декабря 2019 г.

Два исключения - это много?

#cpp #исключения #деструктор


Вычитал у Майерса, то что два исключения в деструкторе - это слишком много.
Что он этим хотел сказать? Одно исключение - это не много, а вот два - это уже много?
Мне кажется, что уже одно исключение не даст приложению нормально работать?
    


Ответы

Ответ 1



Вы его неверно прочли :) В деструкторе вообще не должно быть исключений. Потому что представьте себе - где-то в коде сгенерировано исключение, пошла свертка стека, удаление объектов - т.е. вызовы деструкторов. И если при этом какой-то деструктор сгенерирует исключение - что будет? Будет два исключения, причем второе сгенерировано во время обработки первого. Вот что имеется в виду, когда говорится о двух исключениях. На у а дальше... что же делать дальше? Какое из исключений обрабатывать? Продолжать обработку первого, игнорируя второе? Игнорировать первое и обрабатывать второе? В результате в этой ситуации программа просто аварийно завершается...

понедельник, 9 декабря 2019 г.

Виртуальный деструктор при наследовании

#cpp #деструктор


Класс наследует интерфейс, в котором деструктор объявлен виртуальным. Надо ли в своем
классе явно объявлять деструктор виртуальным? Чтобы обеспечить вызов деструктора при
работе через интерфейс.

class MyInterface{
public:
    virtual ~MyInterface(){};
    ...
}

class MyClass : public MyInterface{
public:
    ~MyClass(); //или же необходимо virtual ~MyClass();
    ...
}

    


Ответы

Ответ 1



Нет, не надо. Он уже и так виртуальный. Начиная с 11-го стандарта, можно в данном случае использовать ключевое слово override. В таком случае, компилятор проверит, является ли деструктор базового класса (или переопределяемый метод в общем случае) виртуальным - и вы сможете отловить ситуацию, когда забыли сделать деструктор базового класса виртуальным.

Ответ 2



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

четверг, 5 декабря 2019 г.

Удаленный деструктор

#cpp #деструктор


Читаю "A Tour of C++" Страуструпа, и наталкиваюсь на стр. 77 на такой совет:


  If a class has a pointer member, it probably needs a user-defined or deleted destructor,
copy and move.


Т.е. вроде бы как 


  Если класс имеет член-указатель, вероятно, ему требуются пользовательские или удаленные
деструктор, копирование и перемещение.


Про копирование и перемещение - нет вопросов, все понятно, про пользовательский деструктор
- тоже.

Но что может означать удаленный деструктор? Не в плане написать

~Class() = delete;


а что это может дать? Ведь такой объект нельзя будет удалить ни автоматом по выходу
из области видимости, ни через delete (понятно, локальный и выделенный через new соответственно).

В чем тут глубокий смысл - создавать бессмертные объекты? :)

P.S. Написал Страуструпу, потребовал объяснений :) Он согласился, что формулировка
не самая точная, предложил вариант


  If a class has a pointer member, consider if it needs a user-defined or deleted
destructor, copy and move;


Как по мне, разница невелика, но тем не менее... В этом плане ответ @lёxölüt ближе
к реалиям, потому принимаю именно его.
    


Ответы

Ответ 1



Смысл вытекает из Правила трёх (пяти). И читать это следует группами (user-defined or deleted) и (destructor, copy and move). Т.е. любую из упомянутых функций следует либо сделать определённой пользователем, либо удалённой. Акцент на удалённом деструкторе получается, видимо, из-за расположения этих слов рядом. То, что этот случай несколько более особенный, чем для копирования/перемещения, можно было бы отразить в исходном предложении, но тогда оно стало бы более многословным. А к чему это ведёт для объекта уже сказано в смежном ответе, да и в самом вопросе, по сути, тоже. Стоит заметить, что при создании объекта в куче (на стеке не получится, т.к. реализация сама вставляет вызов деструктора в код), тем не менее можно избежать сомнительных утечек, достаточно использовать глобальные операторы new/delete и размещающий new: #include struct S { ~S() = delete; }; int main() { void* p = ::operator new (sizeof(S)); // Выделили память S* s = new (p) S; // Сконструировали объект по адресу 'p' ::operator delete(p); // Освободили память }

Ответ 2



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

пятница, 29 марта 2019 г.

Как перехватить исключение

Почему данный код не ловит исключение?
#include
class A { private:
public: A() { std::cout << "A::A()" << std::endl; } ~A() { throw 1; std::cout << "A::~A(int)" << std::endl; } };
int main() { A* p = nullptr;
try { p = new A(); delete p; } catch(...) { std::cout << "catch(...)" << std::endl; }
return 0; }


Ответ

Выбрасывать исключение из деструктора не рекомендуется и на это есть ряд причин.
это может привести к утечкам памяти, невозможно обеспечить какие-либо гарантии безопасности, исключение из деструктора может вылететь во время раскрутки стека, что приведет к разным последствия в зависимости от объекта.
Посмотрим на Ваш код.
delete p;
Как известно, сначала delete-expression вызывает деструктор и затем освобождает память. Если деструктор выбрасывает исключение, то до освобождения памяти дело уже не дойдет и это приведет к утечке.
При реализации, например, контейнеров, невозможно обеспечить какие-либо гарантии относительно безопасности исключений. Например:
vector.push_back();//Вектор был не пуст
Допустим, произошло выделение памяти. Значит старые элементы копируются в новую область памяти. Если копирование прошло успешно, а при уничтожении старых объектов вылетело исключение, то мы получим часть неуничтоженных объектов и утечку памяти. Если же при копировании вылетело исключение и вектор попытается уничтожить уже сконструированные объекты, а при их уничтожении вылетит исключение, то получим ту же утечку, плюс дальше "полетит" уже другое исключение.
Если исключение вылетает из деструктора автоматического объекта во время раскрутки стека, то, в соответствии со стандартом, это приводит к вызову std::terminate
15.2 Constructors and destructors The process of calling destructors for automatic objects constructed on the path from a try block to a throw-expression is called “stack unwinding.” [Note: If a destructor called during stack unwinding exits with an exception, terminate is called (15.5.1). So destructors should generally catch exceptions and not let them propagate out of the destructor. —end note]
Это цитата из C++03, но в последующих стандартах поведение не поменялось.
Начиная с C++11 все деструкторы по-умолчанию имеют спецификацию исключений noexcept. При нарушении этой спецификации вызывается std::terminate:
Whenever an exception is thrown and the search for a handler (15.3) encounters the outermost block of a function with an exception-specification that does not allow the exception, then, — if the exception-specification is a dynamic-exception-specification, the function std::unexpected() is called (15.5.2), — otherwise, the function std::terminate() is called (15.5.1).
То есть, начиная с C++11 любое исключение, покинувшее деструктор, приводит к вызову std::terminate и аварийному завершению программы, тем самым, как минимум, обеспечивая единое поведение и возможность предоставления каких-либо гарантий. Деструктору возможно указать другую спецификацию исключений:
~A() noexcept(false) { /*...*/ }
Но при этом, если вылетит исключение, мы получим все проблемы указанные выше.
Итого: исключение, покинувшее деструктор создает много проблем, и не дает никакой пользы, поэтому общая рекомендация - никогда не позволять исключениям покидать деструктор.