Я программист-любитель - люблю С++ :) От ваших лекций и тонкого юмора испытываю эстетическое удовольствие. Спасибо, что выложили лекции в общий доступ.
добрый день, 31:05 если дать пользователю разный порядок инициализации, то порядок разрушения должен ему соответствовать (быть обратным созданию) так как деструктор один, а конструкторов много, надо было бы выбрать какой порядок инициализации главный, либо сказать что он должен быть везде один. каждый из вариантов ведёт к каким-то сложностям, проще зарубить "на корню" такую возможность.
Здравствуйте! На 34:08 говорится, что мы можем делегировать конструктор и продолжить список инициализации, при условии, что вызываемый конструктор стоит на первом месте. Однако, код со слайда не компилируется (второй конструктор некорректный). Вот что написано в cppreference: If the name of the class itself appears as class-or-identifier in the member initializer list, then the list must consist of that one member initializer only; Как я понимаю, лучшее, что мы можем сделать - это "немного развернуть делегирование" и из конструктора с меньшим числом аргументов, вызывать конструктор с большим их числом.
Здравствуйте! Спасибо за ваш труд, каждую лекцию жду как любимый сериал. Попробовал в godbolt пример про copy-init 1:19:45 . В clang и gcc10 компилирует как у вас в примере. В gcc11 в обоих случаях вставляет "Bar::operator Foo()", в msvc в обоих случаях "Foo::Foo(Bar const &)". Пути компилятора неисповедимы :) За ссылку на godbolt ютуб блокирует комментарии
Да. Поэкспериментировал на GCC. До 17-го стандарта без explicit на operator Foo() имеем Ctor Bar -> Foo Op Bar -> Foo После 17-го стандарта без explicit на оператор Foo() имеем Op Bar -> Foo Op Bar -> Foo До 17-го стандарта с explicit на operator Foo() имеем Ctor Bar -> Foo Ctor Bar -> Foo После 17-го стандарта с explicit на operator Foo() имеем Op Bar -> Foo Ctor Bar -> Foo. Симметричненько получилось. Судя по всему, начиная с 17го стандарта, операторы приведения (явные и неявные) снаружи в "нас" попадают в преимущественные позиции списка перегрузок direct-инициализации для объекта типа "нас". Но и до, и после, direct-инициализации всё ещё пофиг на explicit, а copy -- нет. Это только в контексте GCC. На других не пробовал.
1:19:40 Был убеждён результатами на экране, но вернулся в процессе разбора случая из лекции по исключениям. Набрав этот сниппет у себя, получил иной результат. У вас видимо по умолчанию стандарт С++14 в компиляторе? Выяснил, что при переходе на С++17 стандарт результат стал иным, и в обоих случаях вызывается оператор преобразования. Если расписать подробнее Bar b; // 14 17gcc 17clang Foo f0 (b); // ctor op op Foo f1 {b}; // ctor op op Foo f2 = {b}; // ctor ctor op // в текущем черновике инициализации f1 и f2 идентичны
Foo f3 ({b}); // ctor ctor ctor Foo f4 {{b}}; // ctor ctor ctor Foo f5 = {{b}}; // ctor ctor ctor Foo f6 = b; // op op op На этот раз стандарт не осилил, но кажется лишь в случае copy-initialization могут рассматриваться функции преобразования. Тогда в чем же дело? Единственное предположение, что здесь по какой-то причине вступает в силу copy elision. Поиск в интернете дал вопрос на stackoverflow "Call to conversion operator instead of converting constructor in c++17 during overload resolution", где приводится ссылка на CWG243, дающую ровно ту же мотивацию, что и вы (что выглядит как вызов конструктора им и должно быть). Там же есть ссылка на Bug 82840 для gcc, где дал ответ разработчик gcc. Так в соответствии с P0135R1 (guaranteed copy elision) и в gcc (gcc/cp/call .cc# L12475), и в clang сделали выбор в пользу copy elision, вопреки текущему стандарту. То есть они рассматривают и функции преобразования, как в copy-initialization. Поискав еще немного, нашел архивные сообщения в std-discussion и CWG2327 за авторством Richard Smith, где сказано, что это в действительности упущение в стандарте (что игнорируется copy elision), и его хотят устранить. Фух. И как после такого писать на С++?! P.S. И как после такого не любить С++?🙃 P.P.S. Но почему тогда в gcc f1 и f2 не совпадают?..
Спасибо Вам за прекрасные лекции и подачу материала. Если бы не было NRVO в вашем примере (проверьте с флагами -std=c++14 -fno-elide-constructors на gcc), то вызвалось бы два copy конструктора, а не один (в лекции не упомянули про тот, который на return).
1:08:00 а каким образом можно сделать аргументом конструктора копирования значение? Ведь чтобы его передать как параметр, нужно вызвать конструктор копирования, у которого параметр значение, чтобы его получить надо вызвать конструктор копирования, у которого параметр значение.... Кажется, в этом месте что-то должно сломаться (-:
Спасибо за лекцию, присоединяюсь к мнению насчёт сериала :) Рискну высказать маленькое предложение чисто по визуальному оформлению: не стоит ли включить в vim подсветку синтаксиса (а то и вовсе обмазать плагинчиками)? IMHO такой код и просто легче восприниматься будет, и поаккуратнее выглядеть, и заодно послужит популяризации консольных редакторов (говорю как закоренелый пользователь VS!)
@@tilir а вы долго привыкали к голому тексту, или с самого начала без подсветки работали? Просто уже не в первый раз встречаю тезис, что подсветка не нужна. Чувствуете ли какие-то проблемы, или это просто вопрос привычки? А то тогда тоже попробую отключить подсветку.
Здравствуйте! Спасибо за лекции. На слайде 69 нет деструктора. Он там не нужен или просто не относится к теме? И кажется на слайде 75 маленькая опечатка. В return должен быть x_.
На странице 59 в строке std::copy должно быть std::copy(rhs.p_, rhs.p_ + rhs.n_, p_); Конечно это просто невнимательность но никто из студунтов не заметил.
Здравствуйте, очень хорошие лекции! Вопрос: слайд 25, почему используется десятичный, а не двоичный логарифм при оценки высоты дерева для лучшего случая.
Не подскажете, почему explicit конструктор это экстрим? Сам Страуструп в Core Guidelines советует это делать по умолчанию, clang tidy будет вам настойчиво советовать сделать конструктор explicit, а вы утверждаете полностью противоположные вещи. По-моему, как раз неявное преобразование должно быть хорошо обосновано.
В clang-tidy много странных чекеров, особенно в разделе modernize. Что до core guidelines, я в последний раз заглядывал туда кажется в 2016 и там рекомендовалось всюду использовать array_view. Я как-то больше не открывал. Я исхожу из того, что пользовательские типы должны вести себя как int и float -- в том числе конвертироваться друг в друга если это ничего не стоит. Только если конвертирование сопряжено с затратными операциями -- выделением памяти и всем таким, можно подумать (подумать!) про explicit.
@@tilirАргументация против данных кодстайлов, конечно, интересная, только вот думается, что они и многие другие советуют это делать не просто так. Пользовательские типы точно не должны себя вести как int и float с точки зрения преобразований, даже если не затрагивать всю спорность таких неявных кастов. int это int, просто значение и ничего больше, пользовательский тип практически всегда сложней и конструирование при помощи значения другого типа зачастую не означает их семантическую эквивалентность. Вы говорите, что они должны(!) конвертироваться друг в друга, если это ничего не стоит. Что, даже если это приводит не к тому, чего ожидал программист? Ну а что, сам виноват, забыл что char/bool/double превратится size_t и ты получишь черт знает что. Вы опытный разработчик и, возможно, всегда держите в голове этот и миллион других нюансов языка C++, когда пишете код, но зачем вы настаиваете, чтобы неопытные разработчики оставили этот способ стрельнуть себе в ногу вдобавок ко всем остальным?
Вот именно что в моём опыте неявные преобразования становятся страшными только в одном случае -- когда появляются указатели и инцидентные структуры и нарушается value-семантика. Но это уже не проблема преобразований. Пока value-семантика соблюдена, гайдлайн "вести себя как int" очень полезен. Например он мотивирует зачем ставить const на методы и т.п. И разумеется нет ничего более правильного, чем неявное преобразование int в double. Я с ужасом думаю, что в каждом таком случае пришлось бы писать static_cast. С другой стороны я вполне открыт к новым идеям и сильным аргументам. Если вы посоветуете хороший доклад насчёт того почему везде нужен default explicit (или сами такой запишете -- с примерами и всем таким), то это будет очень позитивно.
Зануление указателей в деструкторе удобно в том случае, если мы случайно обратились к уделенному объекту. Если это произошло через короткое время, то память объекта не успевает распределиться на другой объект и такое обращение вызывает мгновенный Access Violation. Т.е. нам не потребуется сложного дебага. Правда современные компиляторы делают что-то подобное за нас.
@@tilir понятно что гарантий 100% никто не дает, но это повышает шансы того, что ошибка проявиться скорее раньше чем позже. Создайте дебаг проект на Visual Studio, создайте переменную инициализированную нулями с помощью new (например long long) вызывайте delete и смотрите что на самом деле окажется в переменной. Вся переменная будет заполнена байтами 238 / 254. Это происходит строго в функции delete и это документированное поведение для debug сборок на Visual Studio. Это делается намеренно для облегчения отладки. Более того есть ключ _CRTDBG_DELAY_FREE_MEM_DF , как можно понять из названия, куча даже придержит освобожденные блоки, чтоб дать дополнительный шанс выстрелить такому заполненному блоку.
Зануление после delete в линейном участке кода имеет смысл (хотя сам по себе delete в линейном участке это скорее всего ошибка проектирования). Зануление в деструкторе не имеет смысла. Никаких шансов оно не повышает, я на лекции показывал ассемблер.
Спасибо за лекции! На 26:10 Вы говорите, что выведется сначала "default", потом "direct", это видимо оговорка? В теле конструктора `key_ = key` вызовет ведь оператор присваивания
@@tilir Но там ведь direct-инициализация выполняется, чтобы привести `KeyT` к `S`, который ожидает оператор присваивания в качестве правой части. В `key_` новое значение именно присваивается же?
31:05 Ответ очевиден же. В деструкторе переменные класса разрушаются в обратном порядке объявления, поэтому для соблюдения детерминизма пользовательские перестановки в конструкторе явно запрещены.
Интересный аргумент, спасибо. Мне, правда не очень очевидно почему обратный порядок в деструкторе что то значит если пользователь хочет явный список инициализации. Допустим его устраивает такого рода ассимметрия...
@@tilir IMHO. Думается, тогда бы сильно запутались вопросы, связанные с нормативным проектированием классов относительно безопасности исключений? В особенности когда зависимые друг от друга участники списка инициализации деконструировались в обратном порядке их членства, а не списка (а например другие члены класса вообще не были бы упомянуты в списке инициализации). (Для пользователя нет проблем с простой асимметрией в простом случае, но как быть с гарантиями языка в общем случае). Понятно что вопросы исключений появились потом, но это ничего не меняет для интуиции.
@@tilir Если мы в списке инициализации определяем порядок вызова конструкторов полей класса, то это вызовет дополнительные вопросы которые нужно будет решить, например: 1. В классе 15 полей, а я прописываю в список инициализации только 2-а (4-й и 3-й), то не понятно с какого поля начинать вызывать конструкторы 2. Если в списке инициализации можно будет указывать порядок вызова конструкторов, то рано или поздно кто-нибудь захочет список вызова деструкторов полей класса(список деинициализации) и вызывать его нужно будет примерно так: ~Node() {/*тело деструктора*/} : key2_.{}, key1_.{};
на слайдах 60 и 61 разве нет ошибки в функции копирования? Там же должно быть с какого указателя по какой и в какой копируем то есть std::copy(rhs.p_, rhs.p_ + n_, p_);
Приветствую, Константин! Правильно ли я понимаю, если мы говорим о написании надежного промышленного кода, то рекурсия это или smell code, или еще консервативнее - просто неприемлемо?
Здравствуйте. В целом да, сама по себе рекурсия привлекает внимание к коммиту. Но не надо быть слишком категоричным. Я видел как рекурсия проходила ревью и становилась частью промышленного кода там и тогда, где и когда это было оправдано.
В С++ 11 очень полезным нововведением было explicit operator bool, для которого явный контекст определяется компилятором из if, while... Вопрос: имеет ли смысл в каком-либо контексте explicit operator int() или explicit operator MyType()?
Спасибо, интересное дополнение. Писать explicit полезно когда вы хотите заблокировать неявные преобразования. К счастью кроме bool исключений тут не делали. Исключение для bool мне не нравится, т.к. создаёт неприятную асимметрию: struct S { explicit operator bool() { return 1; } // 1 explicit operator int() { return 1; } // 2 ... S s; if (s) { .... } // ошибка для 2 но не для 1 Может быть конечно стоило бы о таком упоминать. Но с другой стороны это слишком распыляющие внимание нюансы. Можно обсудить в комментариях.
Вопросы по слайдам 59 и 60. Если используется int в качестве типа буфера, то нужна проверка на знак? Почему бы не использовать size_t для размера буфера как в stl контейнеразмерах?
Я избегаю лишнего использования беззнаковых типов т.к. операции над ними из-за отсутствия UB плохо оптимизируются. В стандартных контейнерах size_t был ошибкой проектирования, причём довольно дорогой, и, увы, засох в таком виде. Проверка на знак не нужна т.к. по инварианту класса там не может быть отрицательного числа.
@@tilir Спасибо за ответ!) На днях был разговор со старшим коллегой, который также указывал на преимущества оптимизации int по сравнению с uint. Он не настаивал на своём мнение, а я не придал должного ему значения. Впредь буду внимательнее в наших беседах. Спасибо за информацию!) А я всё думал - почему в protobuf тип размера контейнера int? Получается дело в оптимизации?
По поводу объектов нулевого размера. Не очень понятно, как это ломает delete[]. Предположим, я компилятор. Я знаю, что тип пустой (i.e. не хранит данных). Буду считать, что все объекты в массиве начинаются в одном месте. Когда нужно проитерироваться по нему, буду отдавать один и тот же указатель (данных там нет, ничего не ломаю). Когда остановиться я знаю (размер слева от массива лежит). Тут конечно возникают проблемы с реализацией end() для пользовательских типов, т.к. в терминах типа нулевого размера end() недостижим, но как будто и это программисту можно было бы обойти. Короче язык был бы ещё более страшным, но все проблемы как будто можно решать)
Да, это же константная ссылка на себя. [class.copy.ctor] регламентирует это так: non-template constructor for class X is a copy constructor if its first parameter is of type X&, const X&, volatile X& or const volatile X&, [...]
То есть, в примере из лекции компилятор считает тип Copyable совершенно другим типом, даже если параметры шаблонов класса и его конструктора одинаковы? Зачем так сделано? Почему не научить компилятор распознавать подобное?
Я полагаю так сделано из-за ленивого инстанцирования шаблонов. При шаблонном конструкторе копирования компилятор не может, глядя только на определение класса, определить нужен в итоге конструктор по умолчанию или нет т.е. будет этот конструктор инстанцирован или не будет.
Константин, приветствую! Тут назревает (но, надеюсь, не произойдёт) блокировка ютуба. А мне хотелось бы все ваши лекции таки дослушать. Есть ли где-нибудь ещё копия лекций?
@@tilir Ох, из драфта "If a mem-initializer-id designates the constructor's class, it shall be the only mem-initializer; ...Once the target constructor returns, the body of the delegating constructor is executed.", поскольку инициализация всех членов уже выполнена (целевым конструктором) конструктором делегатом. Как это выразить лаконично, я не уверен.
Добрый день, Константин, допустимо ли использовать фигурные скобки в списке инициализации в конструкторе? Node(KeyT key) : key_{key} {} вместо Node(KeyT key) : key_(key) {}
Константин, спасибо за ответ! У меня еще вопрос по делегирующему конструктору. Я попробовал код с презентации (страница 43), немного упростив его: #include struct class_c { int max = 0, min = 0; class_c(int my_max) : max(INT_MAX) {} class_c(int my_max, int my_min) : class_c(my_max), min(INT_MIN) {} // Line 6 }; Но, он не компилируется. g++ -std=c++11 test.cpp test.cpp: In constructor ‘class_c::class_c(int, int)’: test.cpp:6:67: error: mem-initializer for ‘class_c::min’ follows constructor delegation $ clang -std=c++11 test.cpp test.cpp:6:39: error: an initializer for a delegating constructor must appear alone Компилируется только если поменять class_c(int my_max, int my_min) : class_c(my_max), min(INT_MIN) {} на такой вариант class_c(int my_max, int my_min) : class_c(my_max) {} Получается, что делегированный конструктор должен быть не первым, а единственным в списке инициализации. ======================== Еще заметил неточность на страницах 56, 57: {Buffer x; Buffer y = x; } // double deletion Не получится создать объект класса Buffer через Buffer x; Нужно указать количество элементов выделяемого массива, например 5: {Buffer x{5}; Buffer y = x; } // double deletion ======================== И на странице 59 "Реализуем копирование" в конце строки, перед фигурной скобкой не должно быть запятой: Buffer(const Buffer& rhs) : n_(rhs.n_), p_(new int[n_]), { Buffer(const Buffer& rhs) : n_(rhs.n_), p_(new int[n_]) { ======================== Возможно ошибаюсь, но похоже на странице 73 "Пользовательские преобразования", на рисунке, перепутаны направления стрелок.
Примите мои искренние благодарности за Ваш курс лекций. Слушаю сам и рекомендую сотрудникам.
Я программист-любитель - люблю С++ :) От ваших лекций и тонкого юмора испытываю эстетическое удовольствие. Спасибо, что выложили лекции в общий доступ.
посмотрев лекцию, я понял как же много я не знаю . Спасибо вам, что вы делитесь знаниями.
Изучаю по вашим лекциям C++, спасибо огромное за ваш труд.
Ваши лекции - золото!
Спасибо за лекцию, очень интересно.
Константин, спасибо за труд!
Урааа, новая серия :)
31:33
Где-то читал, что порядок инициализации соответствует порядку объявления класса, чтобы деструктор удалял поля в обратном конструированию порядке
Вот бы послушать тот самый "первый курс", на который постоянно делаются отсылки)
Присоединяюсь!
Константин Игоревич, может запишете как-нибудь? :)
добрый день,
31:05
если дать пользователю разный порядок инициализации, то порядок разрушения должен ему соответствовать (быть обратным созданию)
так как деструктор один, а конструкторов много, надо было бы выбрать какой порядок инициализации главный, либо сказать что он должен быть везде один. каждый из вариантов ведёт к каким-то сложностям, проще зарубить "на корню" такую возможность.
Звучит разумно!
Здравствуйте! На 34:08 говорится, что мы можем делегировать конструктор и продолжить список инициализации, при условии, что вызываемый конструктор стоит на первом месте.
Однако, код со слайда не компилируется (второй конструктор некорректный). Вот что написано в cppreference:
If the name of the class itself appears as class-or-identifier in the member initializer list, then the list must consist of that one member initializer only;
Как я понимаю, лучшее, что мы можем сделать - это "немного развернуть делегирование" и из конструктора с меньшим числом аргументов, вызывать конструктор с большим их числом.
Странно что ещё не было в errata, вроде это тут уже находили. Добавил, спасибо.
Здравствуйте! Спасибо за ваш труд, каждую лекцию жду как любимый сериал. Попробовал в godbolt пример про copy-init 1:19:45 . В clang и gcc10 компилирует как у вас в примере. В gcc11 в обоих случаях вставляет "Bar::operator Foo()", в msvc в обоих случаях "Foo::Foo(Bar const &)". Пути компилятора неисповедимы :) За ссылку на godbolt ютуб блокирует комментарии
Очень интересное наблюдение. Надо проверить по стандарту кто прав ))
Да. Поэкспериментировал на GCC.
До 17-го стандарта без explicit на operator Foo() имеем
Ctor Bar -> Foo
Op Bar -> Foo
После 17-го стандарта без explicit на оператор Foo() имеем
Op Bar -> Foo
Op Bar -> Foo
До 17-го стандарта с explicit на operator Foo() имеем
Ctor Bar -> Foo
Ctor Bar -> Foo
После 17-го стандарта с explicit на operator Foo() имеем
Op Bar -> Foo
Ctor Bar -> Foo.
Симметричненько получилось. Судя по всему, начиная с 17го стандарта, операторы приведения (явные и неявные) снаружи в "нас" попадают в преимущественные позиции списка перегрузок direct-инициализации для объекта типа "нас". Но и до, и после, direct-инициализации всё ещё пофиг на explicit, а copy -- нет.
Это только в контексте GCC. На других не пробовал.
1:19:40 Был убеждён результатами на экране, но вернулся в процессе разбора случая из лекции по исключениям. Набрав этот сниппет у себя, получил иной результат. У вас видимо по умолчанию стандарт С++14 в компиляторе? Выяснил, что при переходе на С++17 стандарт результат стал иным, и в обоих случаях вызывается оператор преобразования. Если расписать подробнее
Bar b; // 14 17gcc 17clang
Foo f0 (b); // ctor op op
Foo f1 {b}; // ctor op op
Foo f2 = {b}; // ctor ctor op // в текущем черновике инициализации f1 и f2 идентичны
Foo f3 ({b}); // ctor ctor ctor
Foo f4 {{b}}; // ctor ctor ctor
Foo f5 = {{b}}; // ctor ctor ctor
Foo f6 = b; // op op op
На этот раз стандарт не осилил, но кажется лишь в случае copy-initialization могут рассматриваться функции преобразования. Тогда в чем же дело?
Единственное предположение, что здесь по какой-то причине вступает в силу copy elision.
Поиск в интернете дал вопрос на stackoverflow "Call to conversion operator instead of converting constructor in c++17 during overload resolution", где приводится ссылка на CWG243, дающую ровно ту же мотивацию, что и вы (что выглядит как вызов конструктора им и должно быть). Там же есть ссылка на Bug 82840 для gcc, где дал ответ разработчик gcc.
Так в соответствии с P0135R1 (guaranteed copy elision) и в gcc (gcc/cp/call .cc# L12475), и в clang сделали выбор в пользу copy elision, вопреки текущему стандарту. То есть они рассматривают и функции преобразования, как в copy-initialization.
Поискав еще немного, нашел архивные сообщения в std-discussion и CWG2327 за авторством Richard Smith, где сказано, что это в действительности упущение в стандарте (что игнорируется copy elision), и его хотят устранить.
Фух. И как после такого писать на С++?!
P.S. И как после такого не любить С++?🙃
P.P.S. Но почему тогда в gcc f1 и f2 не совпадают?..
В mingw-10.0.0 такая же фигня, если -std=c++14, с f1 вызывает конструктор, если -std=c++17, c++2a, c++2b - в обоих случаях оператор
Спасибо Вам за прекрасные лекции и подачу материала. Если бы не было NRVO в вашем примере (проверьте с флагами -std=c++14 -fno-elide-constructors на gcc), то вызвалось бы два copy конструктора, а не один (в лекции не упомянули про тот, который на return).
1:08:00 а каким образом можно сделать аргументом конструктора копирования значение? Ведь чтобы его передать как параметр, нужно вызвать конструктор копирования, у которого параметр значение, чтобы его получить надо вызвать конструктор копирования, у которого параметр значение.... Кажется, в этом месте что-то должно сломаться (-:
Да, спасибо, так можно делать только у оператора присваивания. Это я что-то не подумав сказал. Внесу в еррату.
Спасибо за лекцию, присоединяюсь к мнению насчёт сериала :)
Рискну высказать маленькое предложение чисто по визуальному оформлению: не стоит ли включить в vim подсветку синтаксиса (а то и вовсе обмазать плагинчиками)? IMHO такой код и просто легче восприниматься будет, и поаккуратнее выглядеть, и заодно послужит популяризации консольных редакторов (говорю как закоренелый пользователь VS!)
Очень не люблю подсветку синтаксиса, везде её отключаю. Это личное.
@@tilir а вы долго привыкали к голому тексту, или с самого начала без подсветки работали? Просто уже не в первый раз встречаю тезис, что подсветка не нужна. Чувствуете ли какие-то проблемы, или это просто вопрос привычки? А то тогда тоже попробую отключить подсветку.
Ну я не заявляю что отсутствие подсветки лучше. Для многих может быть и правда лучше её наличие. Скорее просто с детства привычка.
Здравствуйте! Спасибо за лекции. На слайде 69 нет деструктора. Он там не нужен или просто не относится к теме?
И кажется на слайде 75 маленькая опечатка. В return должен быть x_.
👍
26:12 - проверил. Сначала direct, потом default)
Константин Игоревич, скажите, пожалуйста, в домашней работе "HWT" нужно использовать декартово дерево?
Да, это самый разумный выбор.
На странице 59 в строке std::copy должно быть
std::copy(rhs.p_, rhs.p_ + rhs.n_, p_);
Конечно это просто невнимательность но никто из студунтов не заметил.
На самом деле в errata это уже есть, значит ошибку кто-то уже нашёл, но всё равно спасибо за внимательность.
@@tilir Здравствуйте, Константин. Также можно использовать функцию std::copy_n(rhs.p_, rhs.n_, p_), которая немного упрощает вариант
Заметил, но авторитет Константина меня придавил. Пошел комментарии читать а тут вы, спасибо))
Здравствуйте, очень хорошие лекции! Вопрос: слайд 25, почему используется десятичный, а не двоичный логарифм при оценки высоты дерева для лучшего случая.
Просто опечатка, там должно было быть logN без указания конкретного основания.
Не подскажете, почему explicit конструктор это экстрим? Сам Страуструп в Core Guidelines советует это делать по умолчанию, clang tidy будет вам настойчиво советовать сделать конструктор explicit, а вы утверждаете полностью противоположные вещи. По-моему, как раз неявное преобразование должно быть хорошо обосновано.
В clang-tidy много странных чекеров, особенно в разделе modernize. Что до core guidelines, я в последний раз заглядывал туда кажется в 2016 и там рекомендовалось всюду использовать array_view. Я как-то больше не открывал.
Я исхожу из того, что пользовательские типы должны вести себя как int и float -- в том числе конвертироваться друг в друга если это ничего не стоит. Только если конвертирование сопряжено с затратными операциями -- выделением памяти и всем таким, можно подумать (подумать!) про explicit.
@@tilirАргументация против данных кодстайлов, конечно, интересная, только вот думается, что они и многие другие советуют это делать не просто так. Пользовательские типы точно не должны себя вести как int и float с точки зрения преобразований, даже если не затрагивать всю спорность таких неявных кастов. int это int, просто значение и ничего больше, пользовательский тип практически всегда сложней и конструирование при помощи значения другого типа зачастую не означает их семантическую эквивалентность. Вы говорите, что они должны(!) конвертироваться друг в друга, если это ничего не стоит. Что, даже если это приводит не к тому, чего ожидал программист? Ну а что, сам виноват, забыл что char/bool/double превратится size_t и ты получишь черт знает что. Вы опытный разработчик и, возможно, всегда держите в голове этот и миллион других нюансов языка C++, когда пишете код, но зачем вы настаиваете, чтобы неопытные разработчики оставили этот способ стрельнуть себе в ногу вдобавок ко всем остальным?
Вот именно что в моём опыте неявные преобразования становятся страшными только в одном случае -- когда появляются указатели и инцидентные структуры и нарушается value-семантика. Но это уже не проблема преобразований. Пока value-семантика соблюдена, гайдлайн "вести себя как int" очень полезен. Например он мотивирует зачем ставить const на методы и т.п. И разумеется нет ничего более правильного, чем неявное преобразование int в double. Я с ужасом думаю, что в каждом таком случае пришлось бы писать static_cast.
С другой стороны я вполне открыт к новым идеям и сильным аргументам. Если вы посоветуете хороший доклад насчёт того почему везде нужен default explicit (или сами такой запишете -- с примерами и всем таким), то это будет очень позитивно.
Зануление указателей в деструкторе удобно в том случае, если мы случайно обратились к уделенному объекту. Если это произошло через короткое время, то память объекта не успевает распределиться на другой объект и такое обращение вызывает мгновенный Access Violation. Т.е. нам не потребуется сложного дебага. Правда современные компиляторы делают что-то подобное за нас.
Если вы случайно обратились к удаленному объекту это ub и последствия непредсказуемы. Вас ничего не спасёт.
@@tilir понятно что гарантий 100% никто не дает, но это повышает шансы того, что ошибка проявиться скорее раньше чем позже. Создайте дебаг проект на Visual Studio, создайте переменную инициализированную нулями с помощью new (например long long) вызывайте delete и смотрите что на самом деле окажется в переменной. Вся переменная будет заполнена байтами 238 / 254. Это происходит строго в функции delete и это документированное поведение для debug сборок на Visual Studio. Это делается намеренно для облегчения отладки. Более того есть ключ _CRTDBG_DELAY_FREE_MEM_DF , как можно понять из названия, куча даже придержит освобожденные блоки, чтоб дать дополнительный шанс выстрелить такому заполненному блоку.
Зануление после delete в линейном участке кода имеет смысл (хотя сам по себе delete в линейном участке это скорее всего ошибка проектирования). Зануление в деструкторе не имеет смысла. Никаких шансов оно не повышает, я на лекции показывал ассемблер.
Спасибо за лекции!
На 26:10 Вы говорите, что выведется сначала "default", потом "direct", это видимо оговорка? В теле конструктора `key_ = key` вызовет ведь оператор присваивания
Нет это не оговорка, это именно direct-инициализация, а не копирование. Та есть ссылочка на слайдах, щёлкните по ней.
@@tilir Но там ведь direct-инициализация выполняется, чтобы привести `KeyT` к `S`, который ожидает оператор присваивания в качестве правой части. В `key_` новое значение именно присваивается же?
@@tommorfin3499 да согласен. Это direct инициализация а ПОТОМ копирование. Действительно можно такое уточнять.
31:05 Ответ очевиден же. В деструкторе переменные класса разрушаются в обратном порядке объявления, поэтому для соблюдения детерминизма пользовательские перестановки в конструкторе явно запрещены.
Интересный аргумент, спасибо. Мне, правда не очень очевидно почему обратный порядок в деструкторе что то значит если пользователь хочет явный список инициализации. Допустим его устраивает такого рода ассимметрия...
@@tilir IMHO. Думается, тогда бы сильно запутались вопросы, связанные с нормативным проектированием классов относительно безопасности исключений? В особенности когда зависимые друг от друга участники списка инициализации деконструировались в обратном порядке их членства, а не списка (а например другие члены класса вообще не были бы упомянуты в списке инициализации). (Для пользователя нет проблем с простой асимметрией в простом случае, но как быть с гарантиями языка в общем случае). Понятно что вопросы исключений появились потом, но это ничего не меняет для интуиции.
@@tilir Если мы в списке инициализации определяем порядок вызова конструкторов полей класса, то это вызовет дополнительные вопросы которые нужно будет решить, например:
1. В классе 15 полей, а я прописываю в список инициализации только 2-а (4-й и 3-й), то не понятно с какого поля начинать вызывать конструкторы
2. Если в списке инициализации можно будет указывать порядок вызова конструкторов, то рано или поздно кто-нибудь захочет список вызова деструкторов полей класса(список деинициализации) и вызывать его нужно будет примерно так: ~Node() {/*тело деструктора*/} : key2_.{}, key1_.{};
на слайдах 60 и 61 разве нет ошибки в функции копирования?
Там же должно быть с какого указателя по какой и в какой копируем
то есть std::copy(rhs.p_, rhs.p_ + n_, p_);
Спасибо, добавил в errata.
Приветствую, Константин! Правильно ли я понимаю, если мы говорим о написании надежного промышленного кода, то рекурсия это или smell code, или еще консервативнее - просто неприемлемо?
Здравствуйте. В целом да, сама по себе рекурсия привлекает внимание к коммиту. Но не надо быть слишком категоричным. Я видел как рекурсия проходила ревью и становилась частью промышленного кода там и тогда, где и когда это было оправдано.
В С++ 11 очень полезным нововведением было explicit operator bool, для которого явный контекст определяется компилятором из if, while...
Вопрос: имеет ли смысл в каком-либо контексте explicit operator int() или explicit operator MyType()?
Спасибо, интересное дополнение.
Писать explicit полезно когда вы хотите заблокировать неявные преобразования. К счастью кроме bool исключений тут не делали. Исключение для bool мне не нравится, т.к. создаёт неприятную асимметрию:
struct S {
explicit operator bool() { return 1; } // 1
explicit operator int() { return 1; } // 2
...
S s;
if (s) { .... } // ошибка для 2 но не для 1
Может быть конечно стоило бы о таком упоминать. Но с другой стороны это слишком распыляющие внимание нюансы. Можно обсудить в комментариях.
Вопросы по слайдам 59 и 60. Если используется int в качестве типа буфера, то нужна проверка на знак? Почему бы не использовать size_t для размера буфера как в stl контейнеразмерах?
Я избегаю лишнего использования беззнаковых типов т.к. операции над ними из-за отсутствия UB плохо оптимизируются. В стандартных контейнерах size_t был ошибкой проектирования, причём довольно дорогой, и, увы, засох в таком виде.
Проверка на знак не нужна т.к. по инварианту класса там не может быть отрицательного числа.
@@tilir Спасибо за ответ!)
На днях был разговор со старшим коллегой, который также указывал на преимущества оптимизации int по сравнению с uint. Он не настаивал на своём мнение, а я не придал должного ему значения. Впредь буду внимательнее в наших беседах. Спасибо за информацию!)
А я всё думал - почему в protobuf тип размера контейнера int? Получается дело в оптимизации?
По поводу объектов нулевого размера. Не очень понятно, как это ломает delete[].
Предположим, я компилятор. Я знаю, что тип пустой (i.e. не хранит данных). Буду считать, что все объекты в массиве начинаются в одном месте. Когда нужно проитерироваться по нему, буду отдавать один и тот же указатель (данных там нет, ничего не ломаю). Когда остановиться я знаю (размер слева от массива лежит). Тут конечно возникают проблемы с реализацией end() для пользовательских типов, т.к. в терминах типа нулевого размера end() недостижим, но как будто и это программисту можно было бы обойти. Короче язык был бы ещё более страшным, но все проблемы как будто можно решать)
Вы вызываете деструктор передавая туда адрес объекта. Вызвав деструктор дважды по одному адресу вы получите double free.
8:30 слайд 29 - это не поисковое дерево, насколько я понимаю, так слева от узла (2) - узел (3), а 3 > 2. Или я ошибаюсь?
Да совершенно точно. Я тут чего-то не то на слайд вынес. Впрочем оно достаточно плохое в плане сбалансированности чтобы демонстрировать идею.
На 1:06:54, что значит "sf rule", нигде не нашел, не разобрал слово
"as-if rule". См. лекцию по стандарту C из соотв. курса: ua-cam.com/video/WAA04Wt48dE/v-deo.html
Здравствуйте. А можно ссылку на видео "as if rule" (если я правильно понял), которое вы упомянули на 1:06:58
ua-cam.com/video/8yUSMJWlEsk/v-deo.html
@@tilir спасибо!
68 слайд.
А если конструктор копирования написать как Copyable(const Copyable &c), он будет считаться "правильным" конструктором копирования?
Да, это же константная ссылка на себя.
[class.copy.ctor] регламентирует это так:
non-template constructor for class X is a copy constructor if its first parameter is of type X&, const X&, volatile X& or const volatile X&, [...]
То есть, в примере из лекции компилятор считает тип Copyable совершенно другим типом, даже если параметры шаблонов класса и его конструктора одинаковы? Зачем так сделано? Почему не научить компилятор распознавать подобное?
Я полагаю так сделано из-за ленивого инстанцирования шаблонов. При шаблонном конструкторе копирования компилятор не может, глядя только на определение класса, определить нужен в итоге конструктор по умолчанию или нет т.е. будет этот конструктор инстанцирован или не будет.
Добрый день! Подскажите на счёт примера на 1:20:20 Попробовал повторить и получилось, что при стандарте > c++14 в обоих случаях вызывается оператор
Да вы правы, залипло из 11 стандарта.
Константин, приветствую! Тут назревает (но, надеюсь, не произойдёт) блокировка ютуба. А мне хотелось бы все ваши лекции таки дослушать. Есть ли где-нибудь ещё копия лекций?
Почитайте вкладку community. Там верхний пост ссылка на телеграм, а во втором-третьем на дискорд.
@@tilir рутуб я нашёл, а дискорд - не сумел. Во втором посте только рутуб :)
@@alex_s_ciframi : discord.gg/vSEp8yW
Добрый вечер. Не нужно ли добавить errata относительно 43 слайда? Как я вижу, на последних выложенных слайдах это отмечено цветом.
Да на 43-м действительно ошибка. Сформулируете мне строчку для errata?
@@tilir Ох, из драфта "If a mem-initializer-id designates the constructor's class, it shall be the only mem-initializer; ...Once the target constructor returns, the body of the delegating constructor is executed.", поскольку инициализация всех членов уже выполнена (целевым конструктором) конструктором делегатом. Как это выразить лаконично, я не уверен.
Добрый день, Константин, допустимо ли использовать фигурные скобки в списке инициализации в конструкторе?
Node(KeyT key) : key_{key} {}
вместо
Node(KeyT key) : key_(key) {}
Да конечно и многие так и делают.
Константин, спасибо за ответ! У меня еще вопрос по делегирующему конструктору. Я попробовал код с презентации (страница 43), немного упростив его:
#include
struct class_c {
int max = 0, min = 0;
class_c(int my_max) : max(INT_MAX) {}
class_c(int my_max, int my_min) : class_c(my_max), min(INT_MIN) {} // Line 6
};
Но, он не компилируется.
g++ -std=c++11 test.cpp
test.cpp: In constructor ‘class_c::class_c(int, int)’:
test.cpp:6:67: error: mem-initializer for ‘class_c::min’ follows constructor delegation
$ clang -std=c++11 test.cpp
test.cpp:6:39: error: an initializer for a delegating constructor must appear alone
Компилируется только если поменять
class_c(int my_max, int my_min) : class_c(my_max), min(INT_MIN) {}
на такой вариант
class_c(int my_max, int my_min) : class_c(my_max) {}
Получается, что делегированный конструктор должен быть не первым, а единственным в списке инициализации.
========================
Еще заметил неточность на страницах 56, 57:
{Buffer x; Buffer y = x; } // double deletion
Не получится создать объект класса Buffer через Buffer x;
Нужно указать количество элементов выделяемого массива, например 5:
{Buffer x{5}; Buffer y = x; } // double deletion
========================
И на странице 59 "Реализуем копирование" в конце строки, перед фигурной скобкой не должно быть запятой:
Buffer(const Buffer& rhs) : n_(rhs.n_), p_(new int[n_]), {
Buffer(const Buffer& rhs) : n_(rhs.n_), p_(new int[n_]) {
========================
Возможно ошибаюсь, но похоже на странице 73 "Пользовательские преобразования", на рисунке, перепутаны направления стрелок.
Спасибо, про делегирующие конструкторы вы правильно заметили. Внесу в errata.
1:04:42
Копирование? Какое копирование? Где копирование? Нет копирования. Забудьте. Не было копирования. 😂
42:24 я тоже так привык делать. Такой совет исходит от сайта raveslicom. Там, на всякий случай после вызова delete, рекомендовали занулять память.
Да много где это советуют. Очевидно они неправы.