ссылка на базовый класс без вирт деструктора
От: Sm0ke Россия ksi
Дата: 22.08.23 15:18
Оценка: 3 (1) :)
До недавнего времени я считал, что если временный объект класса Наследник
привязать к ссылке на константный класс База
то чтобы конечный объект разрушился правильно необходимо в базе задать виртуальный деструктор.

Но оказывается даже если в базе нет никакого явного деструктора, то деструктор наследника всё равно вызовется в этом случае.

Вот тест: https://godbolt.org/z/qncnsce79

В нём за базу взята структура t_param
А за наследника t_error : t_param

#include <iostream>
#include <string_view>

using t_text = std::string_view;
using namespace std::literals::string_view_literals;

struct t_param
{
  // data
  t_text
    status;
};

struct t_error : public t_param
{
  t_error(t_text p) : t_param{p} {}
  ~t_error() { std::cout << "~t_error: " << this->status << '\n'; }
};

void go1(const t_param & p) { std::cout << "go1()\n"; }
void go2(t_param && p) { std::cout << "go2()\n"; }

int main()
{
  const t_param & cv = t_error{"const t_param & cv"sv};
  t_param && rv = t_error{"t_param && rv"sv};
  go1(t_error{"go1(const t_param & p)"sv});
  go2(t_error{"go2(t_param && p)"sv});
  try
  {
    std::cout << "going to throw\n";
    throw t_error{"throw"sv};
  } catch( const t_param & e ) {
    std::cout << "catch\n";
  }
  std::cout << "end\n";
  return 0;
}


Вот output:

ASM generation compiler returned: 0
Execution build compiler returned: 0
Program returned: 0
go1()
~t_error: go1(const t_param & p)
go2()
~t_error: go2(t_param && p)
going to throw
catch
~t_error: throw
end
~t_error: t_param && rv
~t_error: const t_param & cv


Тобишь и по правой сслыке на базу, и по левой константной, — всё равно деструктор наследника отрабатывает.
И как параметр функции, и как throw Наследник catch База.

Это по стандарту? Или компиляторы импровизируют?
Кстати проверено на: clang, gcc, msvc

Если по стандарту, то зачем тогда спрашивается в std::exception деструктор сделан виртуальным?
Может есть какие use кейсы, где это необходимо?
Отредактировано 22.08.2023 17:07 Sm0ke . Предыдущая версия . Еще …
Отредактировано 22.08.2023 15:19 Sm0ke . Предыдущая версия .
Re: ссылка на базовый класс без вирт деструктора
От: rg45 СССР  
Дата: 22.08.23 15:37
Оценка: 16 (2) +3
Здравствуйте, Sm0ke, Вы писали:

S>До недавнего времени я считал, что если временный объект класса Наследник

S>привязать к ссылке на константный класс База
S>то чтобы конечный объект разрушился правильно необходимо в базе задать виртуальный деструктор.

S>Но оказывается даже если в базе нет никакого явного деструктора, то деструктор наследника всё равно вызовется в этом случае.

S>...

Здесь весь фокус в том, что во всех трех случаях ты биндишь ссылки к подобъектам временного объекта (объект базового класса — это тоже подобъект, как и члены-данные). И такой биндинг продлевает жизнь полному объекту:

https://timsong-cpp.github.io/cppwp/class.temporary#6

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]: ссылка на базовый класс без вирт деструктора
От: Sm0ke Россия ksi
Дата: 24.08.23 21:56
Оценка:
Здравствуйте, 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.
Отредактировано 24.08.2023 21:59 Sm0ke . Предыдущая версия . Еще …
Отредактировано 24.08.2023 21:58 Sm0ke . Предыдущая версия .
Отредактировано 24.08.2023 21:57 Sm0ke . Предыдущая версия .
Re[3]: ссылка на базовый класс без вирт деструктора
От: vdimas Россия  
Дата: 25.08.23 08:22
Оценка:
Здравствуйте, Sm0ke, Вы писали:

S>А вот что to complete object of a subobject не знал что без вирт деструктора из базы работает.


Компилятор знает тип полного объекта и обслуживает его lifetime, который ты через ссылку лишь продлил.


S>Тут надо ли в базе делать деструктор виртуальным?


Не обязательно, если юзать тип будешь только по-значению.
Но желательно, "на всякий случай". (С)


S>Вот скажем я делаю либу со своей иерархией типов исключений. В std же они сделали деструкторы виртуальными.


ИМХО, из тех соображений, что любой тип в С++ может использоваться не только для исключений, т.е. что-то типа защиты от дурака.


S>Надо ли мне тоже заводить вирт деструкторы в них обязательно? Чтобы программа работала без UB при всех компиляторах, которые следуют стандарту.

S>Я проверил, что с GCC CLANG msvc — деструторы вызываются в Derived и без виртуальности при катче на Base.

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

Т.е. накладные расходы в STL получаются в лишней строке в vtable у примерно двух десятков типов стандартных исключений.
Отредактировано 25.08.2023 10:51 vdimas . Предыдущая версия .
Re[3]: ссылка на базовый класс без вирт деструктора
От: rg45 СССР  
Дата: 25.08.23 18:07
Оценка:
Здравствуйте, Sm0ke, Вы писали:

S>И ещё вопрос. При throw Derived catch Base

S>аллокация эксепшн объекта как понимаю implementation defined (compiler decides)
S>Тут надо ли в базе делать деструктор виртуальным? Я же не собираюсь объект класса некого my_exception создавать через new.

Честно сказать, я не знаю, для чего деструктор std::exception сделан виртуальным. Время жизни объекта исключения обеспечивается самим компилятором через цепочки обработчиков, независимо даже от способа перехвата исключений (по ссылке или по значению). Не могу даже представить сколько-нибудь реального сценария, в котором мог бы понадобиться этот виртуальный деструктор. Ну разве что только если объект исключения создается в динамической памяти и бросается указатель на объект базового класса. Зачем такое извращение может понадобиться — х.з. — типа сокрытие реального типа исключения? Скорее всего, виртуальным этот деструктор сделан как дань традиции — типа, класс предназначен для полиморфного использования, значит, деструктор должен быть виртуальным.
--
Не можешь достичь желаемого — пожелай достигнутого.
Re: ссылка на базовый класс без вирт деструктора
От: fk0 Россия https://fk0.name
Дата: 25.08.23 23:07
Оценка:
Здравствуйте, Sm0ke, Вы писали:

S>До недавнего времени я считал, что если временный объект класса Наследник

S>привязать к ссылке на константный класс База
S>то чтобы конечный объект разрушился правильно необходимо в базе задать виртуальный деструктор.

S>Но оказывается даже если в базе нет никакого явного деструктора, то деструктор наследника всё равно вызовется в этом случае.


LOL. Хороший тест на (не)знание C++ чтоб спрашивать на собеседываниях.

Наверное уже ответили -- ссылка продлевает срок жизни временного объекта. И деструктор вызывается
вовсе не через ссылку (попробуй руками вызвать через ссылку деструктор вручную...), а у компилятора
припрятан сам объект класса "наследник" в текущем скоупе, просто имени у него нет, зато есть ссылка
на его базовый класс.
Re[4]: ссылка на базовый класс без вирт деструктора
От: fk0 Россия https://fk0.name
Дата: 25.08.23 23:10
Оценка:
Здравствуйте, 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]: ссылка на базовый класс без вирт деструктора
От: rg45 СССР  
Дата: 26.08.23 05:09
Оценка:
Здравствуйте, fk0, Вы писали:

fk0> А как быть с std::exception_ptr? А как быть с возможным копированием исключения пользователем (и последующим удалением)?


И как виртуальный деструктор может помочь в этих сценариях? Можно примерчик?

Это, разве что только, если пользователь разместит копию объекта в динамической памяти, при этом не будет пользоваться ни shared_ptr, ни unique_ptr, никакими другими умными указателями, способными помнить полный тип объекта, при этом сам сделает все возможное, чтобы "потерять" этот тип, тогда да. Так я об этом уже писал выше — кому и зачем может понадобиться так извращаться — не очень понятно.
--
Не можешь достичь желаемого — пожелай достигнутого.
Отредактировано 26.08.2023 17:03 rg45 . Предыдущая версия . Еще …
Отредактировано 26.08.2023 5:55 rg45 . Предыдущая версия .
Отредактировано 26.08.2023 5:43 rg45 . Предыдущая версия .
Re[5]: exception_ptr
От: Sm0ke Россия ksi
Дата: 26.08.23 18:43
Оценка:
Здравствуйте, 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 по неполиморфной базе?

Код: https://godbolt.org/z/1o6oKf6s1
#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 не должно быть копирования объекта самого исключения. У него семантика указателя.

Может я не совсем понял вопрос? Уточните что вы имели ввиду под "копированием исключения пользователем"
Отредактировано 26.08.2023 18:50 Sm0ke . Предыдущая версия . Еще …
Отредактировано 26.08.2023 18:44 Sm0ke . Предыдущая версия .
Re[2]: ссылка на базовый класс без вирт деструктора
От: vdimas Россия  
Дата: 30.08.23 01:19
Оценка:
Здравствуйте, fk0, Вы писали:

fk0>попробуй руками вызвать через ссылку деструктор вручную


#include <iostream>

struct Base {
    virtual ~Base() {
        if(!destroyed) {
            destroyed = true;
            std::cout << "~Base() only!" << std::endl;
        }
    }

    bool destroyed {};
};

struct Derived : Base {
    virtual ~Derived() {
        if(!destroyed) {
            destroyed = true;
            std::cout << "~Derived()" << std::endl;
        }
    }
};

struct AnotherObj : Base {
    virtual ~AnotherObj() {
        if(!destroyed) {
            destroyed = true;
            std::cout << "~AnotherObj()" << std::endl;
        }
    }
};

int main() {
    const Base & obj1 = Derived();
    obj1.~Base();       // виртуальный вызов ~Derived

    new((void*)&obj1) AnotherObj();
    obj1.~Base();       // виртуальный вызов ~AnotherObj

    const Derived obj2 = Derived();
    obj2.~Derived();    // прямой вызов ~Derived
    
    new((void*)&obj2) AnotherObj();
    obj2.~Derived();    // прямой вызов ~Derived

    const Derived obj3 = Derived();
    obj3.Base::~Base(); // прямой вызов ~Base
}

~Derived()
~AnotherObj()
~Derived()
~Derived()
~Base() only!
Отредактировано 30.08.2023 1:31 vdimas . Предыдущая версия .
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.