До недавнего времени я считал, что если временный объект класса Наследник
привязать к ссылке на константный класс База
то чтобы конечный объект разрушился правильно необходимо в базе задать виртуальный деструктор.
Но оказывается даже если в базе нет никакого явного деструктора, то деструктор наследника всё равно вызовется в этом случае.
Тобишь и по правой сслыке на базу, и по левой константной, — всё равно деструктор наследника отрабатывает.
И как параметр функции, и как throw Наследник catch База.
Это по стандарту? Или компиляторы импровизируют?
Кстати проверено на: clang, gcc, msvc
Если по стандарту, то зачем тогда спрашивается в std::exception деструктор сделан виртуальным?
Может есть какие use кейсы, где это необходимо?
Здравствуйте, Sm0ke, Вы писали:
S>До недавнего времени я считал, что если временный объект класса Наследник S>привязать к ссылке на константный класс База S>то чтобы конечный объект разрушился правильно необходимо в базе задать виртуальный деструктор.
S>Но оказывается даже если в базе нет никакого явного деструктора, то деструктор наследника всё равно вызовется в этом случае. S>...
Здесь весь фокус в том, что во всех трех случаях ты биндишь ссылки к подобъектам временного объекта (объект базового класса — это тоже подобъект, как и члены-данные). И такой биндинг продлевает жизнь полному объекту:
The third context is when a reference binds to a temporary object. The temporary object to which the reference is bound or the temporary object that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference if the glvalue to which the reference is bound was obtained through one of the following . . .
Но если ты попробуешь проделать этот трюк напрямую с new и delete (без умных указателей, которые защелкивают знания о типе полного объекта в делетерах), ты получишь UB.
--
Не можешь достичь желаемого — пожелай достигнутого.
Re[2]: ссылка на базовый класс без вирт деструктора
Здравствуйте, rg45, Вы писали:
R>Здесь весь фокус в том, что во всех трех случаях ты биндишь ссылки к подобъектам временного объекта (объект базового класса — это тоже подобъект, как и члены-данные). И такой биндинг продлевает жизнь полному объекту:
R>https://timsong-cpp.github.io/cppwp/class.temporary#6
R>
R>The third context is when a reference binds to a temporary object. The temporary object to which the reference is bound or the temporary object that is the complete object of a subobject to which the reference is bound persists for the lifetime of the reference if the glvalue to which the reference is bound was obtained through one of the following . . .
Про продление lifetime для temporary был в курсе.
А вот что to complete object of a subobject не знал что без вирт деструктора из базы работает.
Спасибо, что разъяснили.
R>Но если ты попробуешь проделать этот трюк напрямую с new и delete (без умных указателей, которые защелкивают знания о типе полного объекта в делетерах), ты получишь UB.
Это понятно)
И ещё вопрос. При throw Derived catch Base
аллокация эксепшн объекта как понимаю implementation defined (compiler decides)
Тут надо ли в базе делать деструктор виртуальным? Я же не собираюсь объект класса некого my_exception создавать через new.
Вот скажем я делаю либу со своей иерархией типов исключений. В std же они сделали деструкторы виртуальными.
Надо ли мне тоже заводить вирт деструкторы в них обязательно? Чтобы программа работала без UB при всех компиляторах, которые следуют стандарту.
Я проверил, что с GCC CLANG msvc — деструторы вызываются в Derived и без виртуальности при катче на Base.
Здравствуйте, Sm0ke, Вы писали:
S>А вот что to complete object of a subobject не знал что без вирт деструктора из базы работает.
Компилятор знает тип полного объекта и обслуживает его lifetime, который ты через ссылку лишь продлил.
S>Тут надо ли в базе делать деструктор виртуальным?
Не обязательно, если юзать тип будешь только по-значению.
Но желательно, "на всякий случай". (С)
S>Вот скажем я делаю либу со своей иерархией типов исключений. В std же они сделали деструкторы виртуальными.
ИМХО, из тех соображений, что любой тип в С++ может использоваться не только для исключений, т.е. что-то типа защиты от дурака.
S>Надо ли мне тоже заводить вирт деструкторы в них обязательно? Чтобы программа работала без UB при всех компиляторах, которые следуют стандарту. S>Я проверил, что с GCC CLANG msvc — деструторы вызываются в Derived и без виртуальности при катче на Base.
В любом случае, когда известен полный тип объекта, деструктор вызывается не как виртуальный, а как обычный.
Т.е. накладные расходы в STL получаются в лишней строке в vtable у примерно двух десятков типов стандартных исключений.
Здравствуйте, Sm0ke, Вы писали:
S>И ещё вопрос. При throw Derived catch Base S>аллокация эксепшн объекта как понимаю implementation defined (compiler decides) S>Тут надо ли в базе делать деструктор виртуальным? Я же не собираюсь объект класса некого my_exception создавать через new.
Честно сказать, я не знаю, для чего деструктор std::exception сделан виртуальным. Время жизни объекта исключения обеспечивается самим компилятором через цепочки обработчиков, независимо даже от способа перехвата исключений (по ссылке или по значению). Не могу даже представить сколько-нибудь реального сценария, в котором мог бы понадобиться этот виртуальный деструктор. Ну разве что только если объект исключения создается в динамической памяти и бросается указатель на объект базового класса. Зачем такое извращение может понадобиться — х.з. — типа сокрытие реального типа исключения? Скорее всего, виртуальным этот деструктор сделан как дань традиции — типа, класс предназначен для полиморфного использования, значит, деструктор должен быть виртуальным.
--
Не можешь достичь желаемого — пожелай достигнутого.
Здравствуйте, Sm0ke, Вы писали:
S>До недавнего времени я считал, что если временный объект класса Наследник S>привязать к ссылке на константный класс База S>то чтобы конечный объект разрушился правильно необходимо в базе задать виртуальный деструктор.
S>Но оказывается даже если в базе нет никакого явного деструктора, то деструктор наследника всё равно вызовется в этом случае.
LOL. Хороший тест на (не)знание C++ чтоб спрашивать на собеседываниях.
Наверное уже ответили -- ссылка продлевает срок жизни временного объекта. И деструктор вызывается
вовсе не через ссылку (попробуй руками вызвать через ссылку деструктор вручную...), а у компилятора
припрятан сам объект класса "наследник" в текущем скоупе, просто имени у него нет, зато есть ссылка
на его базовый класс.
Re[4]: ссылка на базовый класс без вирт деструктора
Здравствуйте, rg45, Вы писали:
R>Здравствуйте, Sm0ke, Вы писали:
S>>И ещё вопрос. При throw Derived catch Base S>>аллокация эксепшн объекта как понимаю implementation defined (compiler decides) S>>Тут надо ли в базе делать деструктор виртуальным? Я же не собираюсь объект класса некого my_exception создавать через new.
R>Честно сказать, я не знаю, для чего деструктор std::exception сделан виртуальным. Время жизни объекта исключения обеспечивается самим компилятором через цепочки обработчиков, независимо даже от способа перехвата исключений (по ссылке или по значению). Не могу даже представить сколько-нибудь реального сценария, в котором мог бы понадобиться этот виртуальный деструктор. Ну разве что только если объект исключения создается в динамической памяти и бросается указатель на объект базового класса.
А как быть с std::exception_ptr? А как быть с возможным копированием исключения пользователем (и последующим удалением)?
Re[5]: ссылка на базовый класс без вирт деструктора
Здравствуйте, fk0, Вы писали:
fk0> А как быть с std::exception_ptr? А как быть с возможным копированием исключения пользователем (и последующим удалением)?
И как виртуальный деструктор может помочь в этих сценариях? Можно примерчик?
Это, разве что только, если пользователь разместит копию объекта в динамической памяти, при этом не будет пользоваться ни shared_ptr, ни unique_ptr, никакими другими умными указателями, способными помнить полный тип объекта, при этом сам сделает все возможное, чтобы "потерять" этот тип, тогда да. Так я об этом уже писал выше — кому и зачем может понадобиться так извращаться — не очень понятно.
--
Не можешь достичь желаемого — пожелай достигнутого.
Здравствуйте, fk0, Вы писали:
fk0>Здравствуйте, rg45, Вы писали:
R>>Здравствуйте, Sm0ke, Вы писали:
S>>>И ещё вопрос. При throw Derived catch Base S>>>аллокация эксепшн объекта как понимаю implementation defined (compiler decides) S>>>Тут надо ли в базе делать деструктор виртуальным? Я же не собираюсь объект класса некого my_exception создавать через new.
R>>Честно сказать, я не знаю, для чего деструктор std::exception сделан виртуальным. Время жизни объекта исключения обеспечивается самим компилятором через цепочки обработчиков, независимо даже от способа перехвата исключений (по ссылке или по значению). Не могу даже представить сколько-нибудь реального сценария, в котором мог бы понадобиться этот виртуальный деструктор. Ну разве что только если объект исключения создается в динамической памяти и бросается указатель на объект базового класса.
fk0> А как быть с std::exception_ptr?
Разве в спецификации к std::exception_ptr есть какие-либо требования, касательно полиморфности (или отсутствия оной) у брошенного класса?
А у std::current_exception() есть ли запрет, когда она была вызвана из catch по неполиморфной базе?
#include <iostream>
#include <source_location>
#include <exception>
using t_source = std::source_location;
std::ostream & operator << (std::ostream & o, const t_source & s)
{
o
<< '[' << s.line()
<< ':' << s.column()
<< "] " << s.file_name()
<< " ~ " << s.function_name();
return o;
}
//struct t_error
{
// data
t_source
source{t_source::current()};
};
struct t_error_range : public t_error
{
enum t_status { n_unset, n_low, n_high };
// data
t_status
status{n_unset};
~t_error_range() { std::cout << "~t_error_range\n"; } // just to show
};
//int main()
{
std::exception_ptr p;
try
{
std::cout << "going to throw\n";
throw t_error_range{.status = t_error_range::n_high};
} catch( const t_error & e ) {
std::cout << "catch from: " << e.source << '\n';
p = std::current_exception();
} catch( ... ) {
std::cout << "catch ...\n";
}
try
{
std::cout << "going to rethrow p\n";
if( p ) { std::rethrow_exception(p); }
} catch( const t_error & e ) {
std::cout << "catch again\n";
}
std::cout << "going to clear p after catch\n";
p = std::exception_ptr{};
std::cout << "end\n";
return 0;
}
Результат:
going to throw
catch from: [46:56] /app/example.cpp ~ int main()
going to rethrow p
catch again
going to clear p after catch
~t_error_range
end
Деструктор брошенного исключения был вызван один раз (при gcc)
fk0> А как быть с возможным копированием исключения пользователем (и последующим удалением)?
Да, при вызове std::rethrow_exception() может произойти копирование объекта исключения (хотя это не обязательно что случится, ибо implementation defined)
Но при копировании объектов std::exception_ptr не должно быть копирования объекта самого исключения. У него семантика указателя.
Может я не совсем понял вопрос? Уточните что вы имели ввиду под "копированием исключения пользователем"