Базовый курс C++ (MIPT, ILab). Lecture 7. Перегрузка операторов

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

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

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

    Больше спасибо за лекции! Я хочу заметить, что на практике Pimpl на unique_ptr можно сделать без определения deleter-а. Идея в том, что нужно разнести точки объявления и определения конструктора и деструктора класса. В h-файл попадает только объявление, а точка определения будет находится в cpp-файле где размер имплементации уже полностью определен.

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

      Да это тоже отличный способ. Наверное надо было его упомянуть. Но он не расширяется на void, а кастомный делетер вполне.

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

    Константин, приветствую! Спасибо за лекцию.
    27:44 - а я для себя как-то давно изобрёл такую запоминалку:
    1) постинкремент "сложнее" (есть копирование объекта, есть аргумент int)
    2) инкремент "проще" (нет копии, нет аргумента int)

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

    Спасибо за лекцию.
    Слайд 59.
    strcmp не обязательно возвращает +1, 0, -1, она возвращает 0, меньшее 0 или большее 0. Специально заглянул в драфт последнего станарта, там ничего не менялось. В godbolt для clang 6.0.0 возвращается не обязательно 0, +1, -1. А дальше почему-то возвращаемое значение начало нормализоваться к этим трем значениям.
    The strcmp function returns an integer greater than, equal to, or less than zero, accordingly as the string pointed to by s1 is greater than, equal to, or less than the string pointed to by s2.

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

      Хорошее замечание. Проверять на == -1 как минимум опасно. Но я вроде нигде на слайдах так не делаю?

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

    Хорошим примером необязательной эквивалентности operator+ и operator+= могут быть атомики: atomic a; a+= 10; это не тоже самое, что a = a + 10;

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

    Спасибо!
    Слайд 73.
    Не спорю с тем что операторы запятая (,), && и || не стоит перегружать, но начиная с C++17 эти перегруженные операторы сохраняют sequence ordering (хотя конечно && и || теряют сокращенные вычисления). Не лазил в стандарт за подтверждением, только посмотрел на cppreference и SO.
    Немного проверил на godbolt, получил что и в gcc и в clang++ до какой-то версии в этих перегрузках нет упорядочивания, а с какой-то версии она есть независимо от того, какой -std указан.

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

      Сейчас глянул. В 14-м ещё стоит сноска "an invocation of an overloaded comma operator is an ordinary function call; hence, the evaluations of its argument expressions are unsequenced" а в 20-м сноски уже нет. Но при этом в [over.oper] я не могу найти так же информации что ordering сохраняется.
      Моя логика тут простая. Перегруженный оператор это функция и [over.oper] не делает разницы между вызовом "a, b" и operator,(a, b) так что логично предположить, что и правила тут обычные (никакого sequencing). Было бы интересно найти в стандарте как же всё-таки это завернуто, что он есть, если есть.

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

      @@tilir Я нашел под нотой понятное общее описание в [expr.pre] 7.1.2:
      [Note 2: Operators can be overloaded, that is, given meaning when applied to expressions of class type or enumeration type. Uses of overloaded operators are transformed into function calls as described in [over.oper]. Overloaded operators obey the rules for syntax and evaluation order specified in [expr.compound], but the requirements of operand type and value category are replaced by the rules for function call. Relations between operators, such as ++a meaning a+=1, are not guaranteed for overloaded operators. - end note]
      Но это под нотой, поэтому не считается.
      Если без ноты, то это есть в [over.match.oper] 12.4.2.3.2:
      If either operand has a type that is a class or an enumeration, a user-defined operator function can be declared that implements this operator or a user-defined conversion can be necessary to convert the operand to a type that is appropriate for a built-in operator. In this case, overload resolution is used to determine which operator function or built-in operator is to be invoked to implement the operator. Therefore, the operator notation is first transformed to the equivalent function-call notation as summarized in Table 15 (where @ denotes one of the operators covered in the specified subclause). However, the operands are sequenced in the order prescribed for the built-in operator ([expr.compound]).
      Из последнего предложения я заключаю что все перегруженные бинарные операторы сохраняют sequence order дефолтных операторов. В том числе и &&, ||, ',' (а также >).
      И как понял разница между вызовом opeartor&&(a, b) и a && b есть. Во всяком случае на gcc порядок для operator&&(a, b) не сохраняется (в отличие от a && b). На cppreference про это тоже есть упоминание. По всей видимости когда мы используем явный синтаксис вызова функции то это трактуется как обычный вызов функции, где порядок вычисления аргументов неопределен и часть про сохранение порядка аргументов built-in оператора не работает.

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

      @@tilir, видимо, вы не заметили мой ответ под видео чернового курса. 7.6.1.3 [expr.call] note-9, 12.2.2.3 [over.match.oper] 2.sentence-4 постулируют это.
      P.S.: Оказывается, давать ссылки на черновик стандарта не лучшая идея, поскольку он довольно часто меняется, и ссылки могут съехать.

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

    Вопрос про перегрузку операторов шаблонных классов (45:32). Почему плохо выражать
    template bool operator==(const Quad& lhs, const T& rhs);
    через
    template bool operator==(const Quad& lhs, const Quad& rhs);
    т.е. примерно так:
    template
    bool operator==(const Quad& lhs, const T& rhs) {
    return lhs == Quad{ rhs };
    }
    ?
    Почему лучше в каждой из функций писать одинаковый код?

  • @LOL-nq3ky
    @LOL-nq3ky 2 ปีที่แล้ว +2

    27:46 - гораздо проще запомнить, что в сигнатуру нужно добавить int ПОСТфактум, чтобы изменить оператор на ПОСТ :)

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

    Можно использовать unique_ptr для Objective-C классов в заголовочном файле, который так же должен быть включен в С++ файлы и мы соответсвенно не можем писать forward declaration. А в .mm файле можно описать свой делитер для Objective-C объекта который скрывается в Pimp unique_ptr. Я по крайней мере использовал только такой способ применения данной техники, интересно было бы почитать про другие.

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

    1:11:03 "Я видел как оператор взятия адреса перегружали для сериализации в файл".
    Не оправдываю такой подход, но скорее всего это было сделано по аналогии с boost serialization. Там один из стандартных подходов к сериализации состоит в использовании перегруженного побитового И (бинарный &). Пользовательский класс реализует метод вроде:
    template void serialize(Archive &ar, unsigned int version) {
    ar & member1 & member2;
    }
    В результате получаем что для сериализации и десериализации достаточно всего одного метода serialize, а семантика (сериализация или десериализация) зависит от семантики переданного класса Archive и того, как он обрабатывает оператор бинарный &.
    Не знаю почему в бусте не сделали через вызов одного метода с переменным шаблонным количеством аргументов, подозреваю что так сложилось исторически и что так выглядит более консистентно с обычными потоками ввода-вывода.

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

      Да, в бусте тоже так любят. Я думаю всё это от того, что запрещено вводить новые операторы. Поэтому людям приходится ломать консистентность старых. Разрешили бы определять кавай вроде operator-=^0_0^=- и к нему ещё Infixity и precendence и сразу бы всем всего хватило.

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

      Скорее всего так.
      В свифте кстати тоже можно создавать новые операторы, задавать им приоритет и так далее. Правда из-за коллизии имен с идентификаторами переменных/функций смайликами новые операторы назвать не получится, там довольно жесткие ограничения на допустимые символы.
      prefix operator -=^-^=-
      prefix func -=^-^=-(value: Int) -> Int { return 42 }
      print(-=^-^=-0) // 42

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

    1:09:02. Слайд 62. Без const не завелость.
    auto operator (const MyInt&) const = default;

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

      Да, в [class.compare.default] стоит требование "non-static const non-volatile member of C". Спасибо за внимательность, добавлю в errata.

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

    Константин Игоревич, с наступившим 2024м Вас.
    41:12, 52:53 а подскажите, как именно упрятка внутрь operator== и навешивание friend обеспечили блокировку неявных преобразований и устранили странное поведение?

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

    Пример с кватерионами как по мне странный, т.к. 1 это не кватерион. Само по себе такое приведение выглядит очень стрёмным.
    К тому же есть гораздо более хороший способ сделать то что вы хотите: фриенд функция, которая заставит декларацию проинстанцироваться заранее (и это уже где-то близко к лупхолам)
    template
    struct foo {
    foo(int) {}
    friend bool operator+(foo, foo) {
    return true;
    }
    };
    Так всё будет работать в обе стороны

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

      P.S. в случае стрингов эти операторы написаны для оптимизации потенциальной в случае const Char*

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

    Слайд 30. Как я понимаю все это стоит в хедере. В cpp мы определяем наш делетер. Тут все понятно. А вот как создать объект MyClass которым будет владеть unique_ptr c? Ведь он инициализирован „nullptr“. С make_unique не получается из-за пользовательского делетера. Подскажите пожалуйста

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

      В конструкторе через new? Но в целом да, хорошо подмечено -- тут по хорошему нужна семантика размещения а до неё ещё далеко. Я вернусь к этом во втором полугодии когда речь пойдёт о шаблонах.

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

    Добрый день. У вас на 24 слайде ошибка. Вместо возврата указателя на функцию gtf в операторе gtfptr_t вы как бы пытаетесь сделать вызов этой функции.

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

    Насчёт оператора ->* , возможно ему придадут в будущем какую то новую для него семантику. И она и будет "интуитивной". Как | для "чейнинга" ренжей или > для вывода и ввода.
    Например, можно заметить, что оператор -> в сущности всегда делает следующее:
    берёт левый операнд и делает std::invoke(right_operand, left_operand) с форвардингом конечно этого всего.
    Действительно
    (v.*ptr)(1, 2) эквивалентно std::invoke(ptr, v, 1, 2);
    Такую же логику имеет -> в концептах
    template
    concept x = requires { { T::value } -> std::same_as; };
    Здесь "оператор" -> подставил тип(аргумент метафункции) в метафункцию std::same_as
    Далее, такую логику можно распространить на всё поведение оператора->, то есть переосмыслить что же он реально делал всё это время(генерализовать)
    А потом перегрузить его скажем для optional
    optional x = foo();
    optional y = x ->* &bar;
    (вызывает функцию если возможно, иначе возвращает пустой опшнл, либо другой вариант, вызывает функцию на значении внутри x, либо же не меняет пустой опшнл)

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

      Чтобы эмулировать логику с (v->*ptr)(args...); оператор ->* должен делать std::bind_front
      template
      std::invoke_result_t operator->*(T&& value, U&& callable) {
      return std::invoke(std::forward(callable), std::forward(value));
      }
      template
      auto operator->*(T&& value, U&& callable) {
      return std::bind_front(std::forward(callable), std::forward(value));
      }
      int main() {
      5 ->* (5 ->* [](auto, auto) { return 0; });
      }

  • @user-et7cm4by2t
    @user-et7cm4by2t 5 หลายเดือนก่อน

    по поводу экзотики, читал как-то статью на хабре, не помню про что. Автор вводил оператор "--->", вроде как только для того, чтобы различать доступ для чтения и доступ для записи. Делал через перегрузку предекремента "--" и доступа к полю по указателю "->". Честно говоря, пока не знаю, как к этому относиться, но выглядит штука немного нездоровой)

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

      Выглядит как интересная идея. Примерчик бы, конечно... Может найдёте?

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

    28:35; слайд 36; errata: при перегрузке унарных пре/пост инкрементов для кватерниона у Вас идентификаторы операторов излишне предворяются ::идентификатором структуры, в которой они уже находятся.
    Не скомпилируется.

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

      Спасибо внёс

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

    И ещё вопрос по 29:53 - а разве оптимизатор не уберёт различие пре- и пост- инкремента в данном случае? А также и сравнение с end() не "упростит"?
    Вообще, я обычно в этих случаях юзаю range-based-for, поэтому, наверное, не обращал внимания на скорость выполнения цикла :)

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

      Я же подчеркнул что это просто джигитовка. Она ничего не стоит и обычно ничего не приносит. Но я всегда смогу сделать искуственный пример где это сыграет и никто ничего не уберет. Вы тоже сможете, попробуйте.

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

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

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

    Впервые познакомился с идиомой PImpl при чтении какой-то из старых книг Саттера. Книга написана в до-C++11 времена и про умный указатель для управления реализацией там не было (или уже не помню). Но мотивация введения идиомы была не сохранение ABI, а уменьшение размера заголовочного файла с интерфейсом класса в основном за счёт удаления ненужных инклудов. И если идти по такому пути максимального избавления от зависимостей, то лучше старый добрый указатель нежели unique_ptr, т.к. экономится заголовочный файл memory а там, например в MSVC, 4300 строк, да реализация самого скрываемого за PImpl класса может быть меньше!

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

      Размер препроцессированного заголовочного файла в наше время это странная мотивация.

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

      @@tilir На Ютубе есть лекции Лакоса из Блумберга на эту тему и книга у него есть Large-Scale C++ Software Design, что-то такое, книга довольно старая, но он готовил второе издание. В проектах уровня tensorflow или pytorch размер начинает иметь значение, когда генерируются объектные файлы по 100-200 мб (на каждую единицу трансляции). Некоторые жалуются что есть 64-128 ядерный сборочный сервер, а работать можно только в 8-16 потоков иначе компилятор выедает всю память. С другой стороны конечно memory довольно популярный заголовок и если в проекте активно используются умные указатели рано или поздно кто-то его подключит, так почему бы и не определение вашего класса. Но идея же в том, что нужно по максимуму исключать инклуды в заголовках, оставляя там только те без которых что называется кушать не могу, т.к. они потом будут включаться везде, куда мы подключаем данный заголовок, как снежный ком.

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

      А 100% разнесение интерфейса и реализации я видел в интеловских библиотеках под брендом oneAPI, та же oneAPI DNN, бывшая MKL DNN. Когда реализация написана на С++ но так, что снаружи доступны только экспортируемые в С функции (реализованные на самом деле на С++). На основе этих функций написана header-only обёртка, реализующая опять же С++ интерфейс, но уже никак не зависящий от кода реализации, только от Сишного API. Мне показалось такое решение очень красивым. Поверх этого же стабильного интерфейса можно писать интероп к любым языкам, хоть к Rust, хоть к Java.

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

      Это называется C API и это полезно. В кланге тоже есть, да много где. Делается именно для гарантий по именам и для использования откуда угодно. Но к PIMPL это отношения не имеет )

  • @НикитаСолоненко-р5ы
    @НикитаСолоненко-р5ы 2 หลายเดือนก่อน

    Здравствуйте! Вопрос касаемо unique_ptr, мы передаем deleter вторым шаблонным параметром, вы сказали, что там идет(за частую) вызов delete, unique_ptr реализован не с помощью allocator_traits?

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

      Увы, unique_ptr не поддерживает аллокаторы. В магистерском курсе на второй лекции про аллокаторы мы углубляемся в эту бездну.

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

    06:22. Здравствуйте, откуда берётся x,y для оператора gtfptr_t()?

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

      Ошибка на слайде. На 6:50 реальный код. На слайде нужно было написать "{ return gtf;}" без вызова с аргументами.

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

    А если отключить оптимизации, не будет ли функция суррогатного вызова обычным косвенным вызовом (т.е. проигрывать функтору)? Я попробовал проверить это, но все три варианта и без оптимизаций, и с ними давали мне одинаковый результат; похоже что оптимизации на уровне процессора "мешают"(?).

    • @user-zv8mr2iw6k
      @user-zv8mr2iw6k ปีที่แล้ว

      тоже не удалось получить различие, жаль, что не удастся блеснуть знаниями таких тонкостей (заинлайнить).

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

    Там, где рассказывается про мотивацию перегрузок, упомянута идиома pimpl. Почему в этой идиоме используют указатель, а не ссылку?

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

      Артем Бочкарев ответил достаточно хорошо. От себя добавлю также ради стабильности ABI. Мы обычно не уверены что такое ссылка как член класса. Может указатель, может и нет.

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

      Если у нас будет ссылка на имплементацию, то пользователь будет обязан передать в конструктор ссылку на имплементацию. А у пользователя "докуметов нет", точнее он вообще не должен знать про имплементацию. С указателем гораздо больше гибкости.

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

    с++ он такой: когда как бы и нет, но, как бы и да.

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

    и можно ли подобные вопросы задавать в дискорде или лучше группировать их "тематически" под роликами с лекциями ? :)
    (хм, кстати, только что заданный вопрос почему-то не видно в ленте. Снова в спам улетело, наверное :D )

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

      Ютуб известен тем, что сносит вопросы со ссылками. Обойти это AFAIC нельзя. Можете присылать такие вопросы мне на email, если там будет важная информация, я выложу со ссылкой на вас.

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

      ​@@tilir Константин, приветствую! Ссылка была - на онлайн компилятор, ага. Вопрос был такой: имеется enum class, и для сериализации или для вывода в поток требуется кастовать его к базовому типу. Поскольку это неудобно, я решил перегрузить унарный плюс (выбрал его, так как "обычно" этот оператор "ничего не делает")
      Хочется понять, какие подводные грабли могут ожидать с таким подходом. Или есть какое-нибудь стандартное решение?
      пример
      #include
      enum class some:int32_t;
      constexpr auto operator+(const some val)noexcept
      {
      return static_cast
      <
      std::underlying_type_t
      <
      std::decay_t
      >
      >(val);
      }
      int main()
      {
      some value{7};
      std::cout

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

      Если нужно только выводить, я бы и перегружал оператор вывода:
      template
      auto& operator

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

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

  • @КириллЧе-я5ы
    @КириллЧе-я5ы ปีที่แล้ว

    Минуточку. А если у нас копия кватерниона? И мы ссылаемся в равенстве на разные адреса?.. но с равными объектами?

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

      Инвариант копирования и присваивания звучит как "эквивалентность во всех контекстах". Поэтому если у вас есть некий объект a и его копия b то если существует такой c, что a == c то должно выполняться и b == c. При этом не требуется даже a == b. Если же ни одного такого объекта нет, то не требуется вообще ничего. Примерно так кстати работает pmr::memory_resource.

    • @КириллЧе-я5ы
      @КириллЧе-я5ы ปีที่แล้ว

      @@tilir спасибо!

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

    48:27 должны, но не обязаны :D

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

    Около 30:00, it++ vs ++it. Имхо, и то и другое отвратительно. Для этого изобрели, например, std::transform (-:

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

      Этот цикл это не обязательно std::transform, мы же не знаем тело.
      Про стандартные алгоритмы будет. Но они не всегда возможны, увы.

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

      @@tilir согласен. Но я призываю к подходу "если хочешь написать цикл, сначала проверь, не написали ли его за тебя в стандартной библиотеке"

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

    std::unique_ptr voidPtr(malloc(sizeof(int)), [](void* ptr) {free(ptr); });

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

      А почему sizeof(int)? Что если я хочу там хранить любой T*?
      Ну и потом удалитель лучше никогда не делать указателем на функцию.

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

    Не раскрыта тема перегрузки многомерных []

    • @Robinzon__Kruzo
      @Robinzon__Kruzo 2 ปีที่แล้ว +4

      Вроде на прошлой лекции Константин рассказывал про доступ через [] на примере матриц.

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

    На слайде 70 ошибка. Вы принизили приоритет оператора присваивания. Также ошибка по поводу порядка вычисления аргументов. Если перегруженный оператор используется по форме встроенного, порядок вычисления сохраняется, в том числе и для операторов ||, && and ,
    #include
    struct tag {};
    void operator , (tag, tag) {}
    struct T { operator tag () { std::cout

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

      С другой стороны отлично, что вы меня пересматриваете ))