Базовый курс C++ (MIPT, ILab). Lecture 12. Безопасность исключений

แชร์
ฝัง
  • เผยแพร่เมื่อ 19 ก.ย. 2024

ความคิดเห็น • 53

  • @alexander4247
    @alexander4247 5 หลายเดือนก่อน +1

    Спасибо за замечательную лекцию!
    20:59 Небольшая поправка английского выражения: "Exception catched" -> "Exception caught".

  • @ivankorotkov2563
    @ivankorotkov2563 2 ปีที่แล้ว +11

    Спасибо за курс!
    21:50 Не совсем так. Если std::uncaught_exceptions() вернул 0, то это гарантия того, что объект не находится в процессе размотки исключений. Но если он вернул не 0, то это вообще говоря не значит что деструктор вызывается в процессе размотки стека и что выброс исключения приведет к std::terminate() несмотря на noexcept(false). Вполне может быть, что выше по стеку идет размотка стека, был вызван деструктор, который вызвал еще что-то, в результате на стеке образовалось еще 100500 стековых фреймов до выброшенного исключения. Тогда из noexcept(false) - деструктора можно легально выбросить исключение и если оно будет обработано до другого исключения, то все будет хорошо. Иными словами, можно иметь одновременно несколько исключений и пока они не пересекутся в одном стековом фрейме std::terminate вызван не будет.
    Слайд 69 (22:54) - не любой деструктор по-умолчанию noexcept. А только те, которые не содержат в своих членах или базовых классах объектов с noexcept(false) деструкторами. То есть эта дрянь неявно транзитивно расползается по использующему коду. Предотвратить дальнейшее расползание можно, явно указывая noexcept у деструктора использующего класса, но тогда если исключение все-же будет выброшено, то вызовется std::terminate. Кстати так помечены деструкторы стандартных умных указателей, поэтому их не стоит использовать с объектами, деструкторы которых бросают исключение, т.к. выброс исключения из деструктора в этом случае приведет к вызову std::terminate несмотря на то, что бросивший деструктор объявлен как noexcept(false) и выше по стеку нет исключения. Что на мой взгляд является еще одним важным моментов в споре о том, почему не надо бросать исключения из деструкторов.
    **Upd**: листал стандарт и в [res.on.exception.handling] нашел что любой класс в стандартной библиотеке должен вести себя, как если у него noexcept деструктор. Значит классы с noexcept(false) деструкторами нельзя полноценно использовать не только с умными указателями, но и с любыми стандартными контейнерами, которые управляют временем жизни объектов.
    23:30 std::current_exception() возвращает объект летящего исключения только внутри блока catch. Внутри деструктора он вернет nullptr даже если деструктор вызван в процессе размотки стека. Соответсвенно завернуть в матрешку летящее выше по стеку исключение в процессе размотки стека не получится.

    • @tilir
      @tilir  2 ปีที่แล้ว +1

      Спасибо за отличный комментарий.
      21:50 в [except.throw]/4 явно сказано: If the exception handling mechanism handling an uncaught exception directly invokes a function that exits via an exception, the function std::terminate is called.
      И далее уточнено для понятливых: Consequently, destructors should generally catch exceptions and not let them propagate.
      Мне кажется вы закладываетесь на детали реализации обработки исключений в вашей системе. Стандарт позволяет аборт где бы по стеку ни было исключение.

    • @ivankorotkov2563
      @ivankorotkov2563 2 ปีที่แล้ว

      Спасибо, это важное примечание. Но кажется оно не противоречит тому, что я написал. Ключевое слово здесь directly. То есть если деструктор был вызван напрямую в процессе размотки стека, то его завершение через исключение вызовет terminate. Но если этот деструктор вызывал другие методы, в том числе и деструкторы, то эти вызовы уже будут косвенным и на них этот текст не распространяется. Если выброшенные исключения будут перехвачены где-то внутри деструктора, который был вызван в процессе размотки стека, то он завершится нормально, а не через исключение и это не приведет к завершению программы.
      Именно это я и имел в виду и кажется это вполне укладывается в процитированный фрагмент стандарта.
      Однако бегло просмотрев стандарт я тоже не нашел прямых указаний, что так можно делать (возможно плохо искал, возможно это выводится из более общих правил). Поэтому напрямую опровергнуть и заявить о своей правоте не могу. Однако в защиту высказанной идеи могу привести следущие доводы:
      1) если так нельзя, то зачем понадобилось в C++17 менять uncaught_exception на uncaught_exceptions (кажется другого способа увеличить счетчик исключений в треде, кроме как бросать исключения в деструкторах в C++17 нет),
      2) в списке [except.terminate] такого запрета тоже нет, насколько я понял.
      Ну и в целом мне кажется это было бы странно если бы в деструкторе мы не могли обработать исключение при условии что оно не приводит к выходу из деструктора.

    • @tilir
      @tilir  2 ปีที่แล้ว +3

      Да, я полагаю я неправильно прочитал ваш ответ. Конечно если вы прямо сейчас в деструкторе и там развиваете некую бурную деятельность, в процессе работы которой опять вызывается некий деструктор и всё это при живом исключении, то там внутри что-то можно кидать если вы это потом поймаете и т.д.
      И для этого сделали тот же uncaught_exceptions.
      Другое дело стоит ли вообще упоминать такие сценарии в бакалаврском курсе? "Просто не делайте этого".

  • @ИгорьПорошин-л6м
    @ИгорьПорошин-л6м 6 หลายเดือนก่อน +1

    В MyVector4 в методе push () произойдет UB. Предположим я создал MyVector v, а затем заполнил его 8-ю не пустыми строками. Далее я вызываю v.push(v[3]), предположим, что в этот момент вектор "решил" реалоцироваться, в объект tmp перемещаются все строки, а наш параметр t (ссылка на v[3]), который планировали добавить, уже пустой (из него переместили строку в tmp[3]).
    Как вариант необходимо сначала выполнять вставку/перемещение объекта t (параметр метода push) в конец tmp.arr_, а затем циклом for остальные объекты

    • @tilir
      @tilir  6 หลายเดือนก่อน

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

  • @stanislavstanislavius7618
    @stanislavstanislavius7618 หลายเดือนก่อน +1

    40:18 фраза: без бутылки водки не разобраться - актуальна как никогда

  • @ОлегВасилевский-ш8щ
    @ОлегВасилевский-ш8щ 2 ปีที่แล้ว

    Большое спасибо за лекцию!
    43:10, слайд 81, очень интересный пример. Можно конструировать объекты, поверх других объектов:
    #include
    #include
    struct MyStruct {
    int a;
    };
    int main() {
    void* raw = ::operator new(sizeof(MyStruct), std::nothrow);
    auto test1 = new (raw) MyStruct{1};
    test1->~MyStruct();
    int* test2 = new (raw) int{2};
    assert(test1->a == 2);
    ::operator delete(raw);
    }
    /* it's magic */

    • @tilir
      @tilir  2 ปีที่แล้ว +1

      Без чего то типа лаундера посередине так лучше не делать. Вы играете с лайфтаймом, это очень опасно.

    • @samolisov
      @samolisov 2 ปีที่แล้ว

      Если бы не вызов assert(test1->a == 2), то std::launder был бы, похоже, и не нужен, объект a умер, ну так его никто больше бы не использовал.

  • @niklkelbon3662
    @niklkelbon3662 2 ปีที่แล้ว +3

    Сначала я подумал что знаю откуда этот код в 48:30... Хм... А потом это оказалось "присваивание Степанова"

  • @vividbw
    @vividbw ปีที่แล้ว

    Здравствуйте, спасибо за интересную лекцию. Позвольте небольшое замечание. На 57:30 в реализации метода push будет UB, если size==used и вставляется элемент этого же самого вектора, так как после реаллокации ссылка на вставляемый элемент будет висячей. Стандартные векторы копируют вставляемый элемент до разрушения прежнего массива.

    • @tilir
      @tilir  9 หลายเดือนก่อน

      Спасибо занёс в errata

  • @alexanderdubovik6666
    @alexanderdubovik6666 4 หลายเดือนก่อน

    1:09:00 - const_cast не следует делать ещё потому, что это нарушает ожидания вызывающего кода.

  • @xsomniator
    @xsomniator 2 ปีที่แล้ว +1

    Спасибо за отличную лекцию! 26:00 Мы можем сдалаь pop следующим образом? Скопировать последний элемент по значению во временную переменную, уменьшить размер вектора, вернуть временную переменную. Здесь будет строгая гаратния безопасности исключений, т.к. при возврате произойдет NRVO и далее не будет вызван ни конструктор копирования, ни конструктор перемещения. Другой вопрос, хотим ли мы копировать по значению... Если так, то мотивация разделить pop на pop и top в исключении копирования по значению, а не в безопасности исключений.

    • @tilir
      @tilir  2 ปีที่แล้ว

      Я боюсь NRVO пока что не обязательное и закладываться на него нельзя. Может сделать, а может и нет.

    • @xsomniator
      @xsomniator ปีที่แล้ว

      ​ @tilir
      С++17: "In the following copy-initialization contexts, a move operation might be used instead of a copy operation: If the expression in a return statement is a (possibly parenthesized) id-expression that names an object with automatic storage duration" [class.copy.elision]
      Здесь might be used, т.е. на усмотрение компилятора.
      С++20 : "In the following copy-initialization contexts, a move operation is first considered before attempting a copy operation: If the expression in a return (8.7.4) or co_return (8.7.5) statement is a (possibly parenthesized) id-expression that names an implicitly movable entity". [class.copy.elision]
      А вот здесь стандарт предписывает сначала попытаться сделать move. Если move возможен в данном контексте, будет выполнен move.
      В C++23 эта фраза из раздела [class.copy.elision] исчезла. Но появился новый термин move-eligible, к которому отнесли return с id-expression, к которому может быть применена операция move [expr.prim.id.unqual]. А вот move-eligible id-expression является xvalue [basic.lval], которое компилятор обязан перемещать как любое другое xvalue.
      Что можете сказать по по поводу этих формулировок? Может быть уже можно делать pop с возвращаемым значением?

  • @lonchakovav
    @lonchakovav 7 หลายเดือนก่อน

    1:22:52 Исключения легко проигнорировать (звук выстрела в ногу): catch(...) { }

  • @tilir
    @tilir  2 ปีที่แล้ว +1

    Внезапно очень серьёзная errata.
    Хелпер destroy в примерах myvec-3 и myvec-4 неверен. Я перезалил на гитхаб корректные примеры.
    github.com/tilir/cpp-graduate/blob/master/08-exceptions/myvec-demo/myvec-3.cc
    См. строчки 28-31

  • @stanislavstanislavius7618
    @stanislavstanislavius7618 หลายเดือนก่อน

    1:08:11 , я правильно понимаю, что на 108 строке мы рекурсивно вызываем push?

  • @viacheslavbarkov6538
    @viacheslavbarkov6538 2 ปีที่แล้ว +1

    Такое интересное замечание.
    Если происходит исключение в конструкторе, деструктор не вызывается. Вызываются только деструктор в виртуальных базовых, базовых классов и вложенных объектов.
    Но если конструктор делегирующий, и в его теле происходит исключение, деструктор вызван будет!
    14.3.3 14.3.4

    • @tilir
      @tilir  2 ปีที่แล้ว

      Действительно интересное замечание! Не знал.

    • @viacheslavbarkov6538
      @viacheslavbarkov6538 2 ปีที่แล้ว

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

    • @samolisov
      @samolisov 2 ปีที่แล้ว +1

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

  • @vividbw
    @vividbw ปีที่แล้ว

    На 1:11:55, вероятно, должно быть is_nothrow_move_constructible, так как push вызывает перемещающий конструктор

    • @tilir
      @tilir  9 หลายเดือนก่อน

      Зависит от реализации MyVector. Для примера к этим слайдам там вроде присваивание.

  • @bochkarevartem
    @bochkarevartem 2 ปีที่แล้ว

    Не то, чтобы это особо на что-то влияло, но на 1:14:45 функция void push(T&& t) будет инстацирована только если она где-то вызывается. То есть теоретически таким вектором можно очень долго пользоваться для "неправильных T". При этом если некоторые другие функции тоже подразумевают такое ограничение, но static_assert есть только тут, то это не очень хорошо))

    • @tilir
      @tilir  9 หลายเดือนก่อน

      Да но это то с чем мы всё время живём из-за ленивого инстанцирования.

  • @philippmikoian7065
    @philippmikoian7065 9 หลายเดือนก่อน

    56:55 "Неприятный бранч, бранч по параметру функции, он отвратительно предсказывается".
    Подскажите, пожалуйста, по каким словам можно погуглить про бранч предикторы, которые принимают в расчёт выражения в условном операторе? Мои знания по архитектуре ограничиваются предсказателями, которые предсказывают только основываясь на адресе инструкции в памяти и истории ветвлений для неё. Я правильно понимаю, что для них предсказания, при условии редких присваиваний объекта самому себе, будут иметь неплохую точность?

    • @tilir
      @tilir  9 หลายเดือนก่อน

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

  • @lonchakovav
    @lonchakovav 7 หลายเดือนก่อน +1

    Скажите, почему "std" вы произносите как "студ"? Англ. эс-ти-ди или лат. эс-тэ-дэ. *Гугл понять (мне) не помог.

    • @tilir
      @tilir  7 หลายเดือนก่อน +1

      Так принято на англоязычных конференциях и созвонах. Эстэдэ никто не поймёт.

    • @eugenelukash
      @eugenelukash 6 หลายเดือนก่อน

      это звучит ближе к слову "стыд" ))

  • @DENRENMaster
    @DENRENMaster 2 ปีที่แล้ว

    Отличное видео! Правда есть вопрос на 1:13:25. Почему в метод push передаётся const T& t ?) Мы же собираемся по возможности перемещать

    • @tilir
      @tilir  2 ปีที่แล้ว +1

      Я тут предполагаю что push_move написан как на слайдах 96-97 через временную переменную и move-from уже её.

    • @DENRENMaster
      @DENRENMaster 2 ปีที่แล้ว

      @@tilir Понял, спасибо

  • @Konstantin_Evdokimov
    @Konstantin_Evdokimov 9 หลายเดือนก่อน

    На 4 минуте в методе push видимо фигурная скобка пропущена перед tmp.push(t);

    • @tilir
      @tilir  9 หลายเดือนก่อน

      Нет ничего не пропущено. Фигурная скобка закрывается где должна.

    • @Konstantin_Evdokimov
      @Konstantin_Evdokimov 9 หลายเดือนก่อน

      @@tilir действительно не нужна, сразу не понял как это работает.

  • @andreyt2237
    @andreyt2237 ปีที่แล้ว

    48:36 Получается, если в деструкторе всегда оставлять объект в консистентном состоянии, то присваивание Степанова в принципе не сделает ничего непоправимого? Если вынести за скобки сложности поддержки такого кода.

    • @tilir
      @tilir  9 หลายเดือนก่อน

      Деструктор не может оставлять объект в консистентном состоянии. Он заканчивает его время жизни. Объект с закончившимся временем жизни не консистентен ни в каком случае.

  • @samolisov
    @samolisov 2 ปีที่แล้ว

    57:41, прикольно, что в операторе присваивания для Controllable вы используете std::swap, а в операторе присваивания вектора - просто swap, при этом определения swap для MyVector при прокручивании слайдов я не увидел. Это - тот же std::swap, но найденный через ADL? Но в таких случаях обычно внутри метода пишут using std::swap, внося это имя в скоуп, у вас же этого нет. Не понимаю, как тогда компилятор находит нужный swap? Влияет ли здесь тот факт, что MyVector объявлен в глобальном пространстве имён?

    • @tilir
      @tilir  9 หลายเดือนก่อน

      Конечно надо писать std::swap. Тут мне повезло, находит по обычному поиску имён в охватывающем пространстве.

  • @alexanderdubovik6666
    @alexanderdubovik6666 4 หลายเดือนก่อน

    Кажется, что гарантия RVO, предоставляемая в c++17, делает неактуальным резбиение top() и pop() на две функции.

    • @tilir
      @tilir  4 หลายเดือนก่อน

      Вы путаете RVO и NRVO.

  • @samolisov
    @samolisov 2 ปีที่แล้ว

    Ещё немного смутило зачем вы вносите поля базового класса в скоуп дочернего с помощью using? Они же и так должны там быть видны. Я понял для методов есть проблема т.к. если объявили внутри дочернего свой метод с таким же именем, то методы базового не будут участвовать в перегрузке. Но для полей же такой проблемы нет, поля нельзя перегружать. Это защита чтобы случайно не объявить такие же поля внутри дочернего класса (без using при таком конфликте clang++ молча использует поле дочернего, с using - ругается)?

    • @bochkarevartem
      @bochkarevartem 2 ปีที่แล้ว +1

      Там наследование от шаблонного класса, для этого случая есть специальное правило, что они не должны быть видны. Но в зависимости от версии и флажков компилятора они все же могут быть видны. В msvc есть флаг /permissive- , который приводит многие вещи в соответствие со стандартом. Иначе по умолчанию вижуал студия компилирует много всего нехорошего, что потом после коммита ломаются билды gcc и clang. У последних аналогичный флаг есть, только значение по умолчанию другое.

    • @samolisov
      @samolisov 2 ปีที่แล้ว +1

      @@bochkarevartem хм, спасибо. Понял так, что зависит только от дочернего класса, если он - шаблон, то поля базового (даже если это обычный класс) надо вводить через using, ну или обращаться через this. Спасибо двухфазному разрешению имён.

  • @niklkelbon3662
    @niklkelbon3662 2 ปีที่แล้ว

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

    • @tilir
      @tilir  2 ปีที่แล้ว

      Их должны этому учить на первом курсе в рамках языка C. Тут вопрос времени занятий: если мы это повторяем то что-то мы явно выбрасываем =)

  • @ddvamp
    @ddvamp ปีที่แล้ว

    17:18 Кажется по стандарту это всё же UB [dcl.ref]#note-2 [Note 2: In particular, a null reference cannot exist in a well-defined program, because the only way to create such a reference would be to bind it to the “object” obtained by indirection through a null pointer, which causes undefined behavior. As described in [class.bit], a reference cannot be bound directly to a bit-field. - end note]
    P.S. Или разговор про дереференс в невычислимых контекстах?
    P.P.S. Кстати почему я удивился typeid, потому что в отличие от других случаев, там разыменование происходит не в качестве unevaluated operand (т.е. считай вычислимое выражение)

    • @tilir
      @tilir  ปีที่แล้ว

      Вот просто никогда не читайте стандарт про разыменование nullptr поверхностно. Там масса интересного и многое приходится соскребать по кусочку. Например исключение для typeid явно прописано.