С перемещениями могут быть проблемы...например вы пишете такой код: { constexpr size_t SZ = 100; int arr[SZ]; FillArray(1, 500, arr, SZ, true); Vector vec(arr, SZ); } Во второй строке массив заполняется что не играет роли. Ошибка в 4-ой строке. У шаблона Vector есть конструктор перемещения. Он берёт и указатель на int взаимствует присваивая ему nullptr как обычно это делается. А когда код выходит из области видимости вектора разумеется вызывается деструктор который освобождает память. и тут возникает исключение потому что память эта не из динамической памяти а из статической и по сути на стеке если нее глобальная. Это было бы если бы мы вынесли определение arr туда. Просто ошибка происходила бы при закрытии программы. Как исправить? Во первых у Vector дб ещё и обычный конструктор с копированием и таким же списком. Но в качестве аргумента надо написать не Vector(T *&&ar, const size_t sz) а Vector(соnst T *ar, const size_t sz) и если будет вызван 2-ой конструктор, то проблем не будет. Но создаст динамическую память в неё запишет значения а потом в деструкторе её освободит. внешне вызов выглядит одинаково они не различимы. и компилятор будет стабильно вызывать конструктор перемещения, а должен вызывать конструктор копирования. Из lvalue можно сделать rvalue, а вот наоборот нельзя!!! потому что у rvalue нет адреса... По крайнй мере при вызове. Но если преобразовать arr в константный указатель это заставит компилятор вызвать именно конструктор с копированием, а не перемещением. И проблема будет решена. А как по другому я не знаю...
А не странно то что ядро большенство перечистлених программ написан на чистом C. почему все написанное на С пречистлается к С++... не честно же... протесту...
Давайте разбираться в этом. Нас интересует вот этот пункт стандарта: eel.is/c++draft/class.temporary#6.4 Если вкратце, то если мы захватываем ссылку на поле временного объекта, доставая его через оператор "точка", то ссылка продлевает время жизни как временного объекта, так и вектора, поэтому все хорошо. godbolt.org/z/NSz6s7 Но если мы захватываем ссылку, которая возвращает функция (ссылка на поле класса), то тут уже "ква" - ловим Undefined Behavior. godbolt.org/z/QHzmoj Спасибо, что заметили, в лекциях поправим это место :)
Почему в С++ отказались от инициализации полей структур по именам полей? В простом С очень удобно. Т.е. struct rect textRect = { .Left = 0, .Top = 0, .Width = 320, .Height = 100 } в простом C возможно, а в плюсах нет.
В C++20 таки подвезли: en.cppreference.com/w/cpp/language/aggregate_initialization#Designated_initializers Однако есть небольшое ограничение в отличие от языка C - инициализаторы полей должны идти в том же порядке, в котором объявлены поля. Почему именно так? Ответ прост - конструкторы: то, в каком порядке создаются поля классов, отличается в этих языках. Подробнее здесь - en.cppreference.com/w/cpp/language/initializer_list#Initialization_order.
PVS studio объясните пожалуйста вот такой момент, про создании лямбды и указании ввиде списка захвата & как именно будет формироваться класс на уровне компилятора, тоесть как он в поля запихнет все что есть снаружи?
я имею ввиду rfor. Нет не по любой коллекции, по массиву нельзя если в данном месте компилятор не знает его размера. Например при передаче его в функцию через указатель. Так же по динамическому массиву нельзя... Но если написать например int *arr[]{1, 2, 3, 10, -5};(в с++20 так можно) то rfor работает. Потому что тут компилятор знает его размер на этапе компиляции. И да, этот указатель можно мувить в отличие от указателя на статическую память. Хорошо ведь правда? Вот...
Для примера на 16:24 разве не достаточно вынести создание объекта std::pair за цикл, а в цикле уже присваивать второй паре значение i и этот объект дальше передавать в вектор. Наивная, но эффективная оптимизация..
В целом, да, для этого примера такую оптимизацию можно сделать. При этом на каждой итерации будет вызываться конструктор копирования std::pair, что вызовет конструктор копирования MyString. Однако цимес данного примера в том, что до C++11 если в функцию передавали временный объект как фактический аргумент, а формальный аргумент являлся ссылкой на константу, то компилятор с этим не мог ничего поделать, и производил копирование временного объекта, что "дорого". Собственно, этот хитрый "бенчмарк" для этого и предназначен (на самом деле надо было в примере передавать не одну и ту же строку в конструктор пары). Проблема была очень насущной, и приходилось использовать разные ухищрения для устранения ненужного копирования, например COW (Copy on Write) - в строчку добавляли счетчик ссылок, и при копировании строки производилось его увеличение, в деструкторе - уменьшение. Если строчку каким-либо образом меняли, то создавался новый экземпляр строки, счетчик для новой строки приравнивался единице. Как счетчик ссылок равнялся нулю - динамически аллоцированную строчку прибивали.
На 58:00 непонятно, разве компилятор не должен сгенерировать за нас конструкторы и операторы присваивания по умолчанию, то есть мы просто можем опустить объявление этих конструкторов? И зачем тогда default...
Если определить какой-нибудь конструктор вручную, то конструктор по умолчанию, простите за тавтологию, не генерируется по умолчанию. Чтобы указать что его все равно нужно сгенерировать, нужен будет default.
Речь идёт о spaceship operator (en.cppreference.com/w/cpp/language/default_comparisons#Defaulted_three-way_comparison). Более подробно останавливались на нём в лекции 12 (th-cam.com/video/KPuYn_fUdxc/w-d-xo.html&ab_channel=PVS-StudioRu). Если коротко - теперь можно написать следующий код: struct SomeStruct { int n; std::string s; float d; auto operator(const SomeStruct &) const = default; }; А всё :) Теперь компилятор сможет за вас сгенерировать тела всех операторов сравнения. Кода стало заметно меньше. Если требуется более сложная кастомная логика - просто пишем свою имплементацию.
Да, в том примере для сортировки по убыванию действительно можно воспользоваться функциональным объектом "std::greater", даже до C++11. Однако если потребуется более сложная логика, до C++11 пришлось бы реализовывать кастомный класс с перегруженным "operator()". Скорее всего, вы бы не хотели, чтобы этот класс был виден снаружи вашей функции, т.е. класс бы пришлось делать локальным. C++11 добавил анонимные функции, которые делают все то же самое за вас. Если копнуть еще глубже, то лямбды имеют преимущество над локальными классами. Например, у последних имеется вот такой список ограничений (en.cppreference.com/w/cpp/language/class#Local_classes). Наиболее существенный из них - невозможность определять внутри класса шаблоны. Обобщенные лямбды из C++14 не имеют такого недостатка, и можно смело использовать (godbolt.org/z/rfbErEYxE) ключевое слово "auto" при объявлении формальных параметров. Объявление хотя бы одного такого формального параметра делает перегруженный оператор внутри лямбды шаблоном.
Я так понимаю все благодаря boost библиотекам, либо благодаря ранним официальным анонсам || инсайдерской инфе. Что-то вроде того, когда все за полгода до выхода уже знают дизайн следующего айфона)
Ну мы же понимаем, что выражение TimeKeeper time_keeper(Timer()); не есть определение функции которая возвращает TimeKeeper и принимает на входе другую функцию возвращающую Timer и не принимающую никаких аргументов. Т.е. по сути Timer(*fun)() или TimeKeeper time_keeper(Timer(*fun)()) для пущей убедительности. Вот щаз она точно выглядит как декларация функции с именем time_keeper. Но у этой функции нет метода get_time. У функции я должен сказать вообще нет никаких методов в отличие от TimeKeeper у которого такой метод есть. Вот на это компилятор и ругается. Чего вы там совсем ку-ку вызываете метод у функции? К слову сказать у функционального объекта могут быть методы и поля, а у просто функции нет... Но мы-то вовсе не это хотели написать а определение экземпляра класса TimeKeeper с инициаллизацией которая описана в его конструкторе. Но как это объяснить компилятору если у него в приоритете функциональный подход а потом классовый. Хотя тов Ленин учил, что ко всему нужен классовый подход. И если бы компилятор так сделал как учил наш великий учитель тов Ленин то компилятор бы не запутался в 3-х соснах а сделал бы всё правильно... А не решил что это какая-то непонятная ХРЕНЬ и потом начал на эту ХРЕНЬ ругаться, которой там даже не предполагалось. О как! Ну не всё хорошо делает компилятор, но они над этим работают и пытаются устранить... Пока 23-ая версия пошла. Модули я ещё не пробовал. Но на паскале писал когда-то... Там всё примерно так как сказано в описании. Впрочем я это сразу заметил. на Фортране тоже модули.. И на Питоне с перлом. И только на плюсах хэдера. В связи с этим вопрос, а как dll писать? Ведь там это принципиально важно. И не только там... Если нет хэдеров то как же тогда? Или сделают ещё один формат выполняемых, подключаемых файлов? В паскале это TPU блин... 1994 год глубокое средневековье, а там модули!!! Я всегда говорил, что всё новое это просто хорошо забытое старое. Мы ходим по кругу... Ничего принципиально нового не выдаём... Всё где-то когда-то как-то было.
Ну мы же понимаем, что выражение TimeKeeper time_keeper(Timer()); не есть определение функции которая возвращает TimeKeeper и принимает на входе другую функцию возвращающую Timer и не принимающую никаких аргументов. Это человек понимает семантически, что здесь декларируется объект, потому что это было его желание. А вот стандарт C++, по которому реализованы компиляторы, здесь не согласен. И дело в тяжком наследии C. C++ исторически создавался как расширение C, поэтому грамматика была переиспользована. И к сожалению, по стандарту здесь предпочитается грамматическое правило по разбору декларации функции, поскольку не встретилось ничего, что могло бы остановить парсинг согласно этому правилу. Вы можете попробовать изменить это, отправив предложение в стандарт: stdcpp.ru/proposals Но успеха здесь ждать не стоит. Ведь C++ развивается с ориентацией на обратную совместимость с языком C. Только создавать другой диалект C++, как например этим занимается Герб Саттер (C++2): github.com/hsutter/cppfront
В связи с этим вопрос, а как dll писать? Примерно так же, как и раньше :) Итоговое приложение или динамическая библиотека получается после линкера. Здесь ничего не поменяли. Изменили этап компиляции с применением модулей. Модуль состоит из интерфейсного файла и имплементации. Когда модуль компилируется, компилятор формирует 2 файла: обычный объектный файл для линковки и BMI (Binary Module Interface). Первый всё также передается линкеру, а вот второй использует компилятор, когда встречает import в компилируемом файле. Т.к. модуль не меняется из-за внешних макроопределений (в отличие от заголовочных файлов), это позволяет сериализовать некоторое промежуточное представление в BMI и десериализовывать его, не тратя время на повторный разбор кода.
Отличная лекция. Посмотрел и еще потом пересмотрю
Спасибо, что делитесь! Интересно послушать настоящих практиков о их понимании С++.
Довольно качественная лекция, и качественно сделанный для понимания ролик.
Офигенные лекции. Совершенно случайно нашел, гуглив метапрограммирование)
огромное пасибо, Филипп.👍
огромное пасибо, Филипп. клевая лекция
Топовая лекция! Спасибо!
Спасибо! Отличная лекция!
Спасибо, человечище!
С перемещениями могут быть проблемы...например вы пишете такой код:
{
constexpr size_t SZ = 100;
int arr[SZ];
FillArray(1, 500, arr, SZ, true);
Vector vec(arr, SZ);
}
Во второй строке массив заполняется что не играет роли. Ошибка в 4-ой строке. У шаблона Vector есть конструктор перемещения. Он берёт и указатель на int взаимствует присваивая ему nullptr как обычно это делается. А когда код выходит из области видимости вектора разумеется вызывается деструктор который освобождает память. и тут возникает исключение потому что память эта не из динамической памяти а из статической и по сути на стеке если нее глобальная. Это было бы если бы мы вынесли определение arr туда. Просто ошибка происходила бы при закрытии программы. Как исправить? Во первых у Vector дб ещё и обычный конструктор с копированием и таким же списком. Но в качестве аргумента надо написать не Vector(T *&&ar, const size_t sz) а Vector(соnst T *ar, const size_t sz) и если будет вызван 2-ой конструктор, то проблем не будет. Но создаст динамическую память в неё запишет значения а потом в деструкторе её освободит. внешне вызов выглядит одинаково они не различимы. и компилятор будет стабильно вызывать конструктор перемещения, а должен вызывать конструктор копирования. Из lvalue можно сделать rvalue, а вот наоборот нельзя!!! потому что у rvalue нет адреса... По крайнй мере при вызове. Но если преобразовать arr в константный указатель это заставит компилятор вызвать именно конструктор с копированием, а не перемещением. И проблема будет решена. А как по другому я не знаю...
Хорошие лекции, спасиб.
Единственная просьба - хватит проталкивать)
Спасибо! Это круто!
Больше всего понравилось то, что спикер похож на Тесака. Доклад тоже хороший.
Я не один кто также подумал
XD
топовый чел
А не странно то что ядро большенство перечистлених программ написан на чистом C.
почему все написанное на С пречистлается к С++... не честно же... протесту...
круто 👍
57:32 опечатки в коде?
48:45.
У меня не удалось воспроизвести проблему :(
Давайте разбираться в этом. Нас интересует вот этот пункт стандарта: eel.is/c++draft/class.temporary#6.4
Если вкратце, то если мы захватываем ссылку на поле временного объекта, доставая его через оператор "точка", то ссылка продлевает время жизни как временного объекта, так и вектора, поэтому все хорошо.
godbolt.org/z/NSz6s7
Но если мы захватываем ссылку, которая возвращает функция (ссылка на поле класса), то тут уже "ква" - ловим Undefined Behavior.
godbolt.org/z/QHzmoj
Спасибо, что заметили, в лекциях поправим это место :)
Разве Яндекс браузер не тот же хром ?
Почему в С++ отказались от инициализации полей структур по именам полей? В простом С очень удобно. Т.е. struct rect textRect = { .Left = 0, .Top = 0, .Width = 320, .Height = 100 } в простом C возможно, а в плюсах нет.
В C++20 таки подвезли: en.cppreference.com/w/cpp/language/aggregate_initialization#Designated_initializers
Однако есть небольшое ограничение в отличие от языка C - инициализаторы полей должны идти в том же порядке, в котором объявлены поля. Почему именно так? Ответ прост - конструкторы: то, в каком порядке создаются поля классов, отличается в этих языках.
Подробнее здесь - en.cppreference.com/w/cpp/language/initializer_list#Initialization_order.
PVS studio объясните пожалуйста вот такой момент, про создании лямбды и указании ввиде списка захвата & как именно будет формироваться класс на уровне компилятора, тоесть как он в поля запихнет все что есть снаружи?
th-cam.com/video/VE3go7qcG04/w-d-xo.htmlsi=zd7HzYkWv_BeupdA&t=2023
Вы опередили своим вопросом нас)
Божественно.
А можно написать, как называется проблема со скобками - most что-то там...? Не расслышала...
Не подскажите таймкод? :)
Полностью не смотрел, но может most vexing parse?
я имею ввиду rfor. Нет не по любой коллекции, по массиву нельзя если в данном месте компилятор не знает его размера. Например при передаче его в функцию через указатель. Так же по динамическому массиву нельзя... Но если написать например int *arr[]{1, 2, 3, 10, -5};(в с++20 так можно) то rfor работает. Потому что тут компилятор знает его размер на этапе компиляции. И да, этот указатель можно мувить в отличие от указателя на статическую память. Хорошо ведь правда? Вот...
Для примера на 16:24 разве не достаточно вынести создание объекта std::pair за цикл, а в цикле уже присваивать второй паре значение i и этот объект дальше передавать в вектор. Наивная, но эффективная оптимизация..
В целом, да, для этого примера такую оптимизацию можно сделать. При этом на каждой итерации будет вызываться конструктор копирования std::pair, что вызовет конструктор копирования MyString.
Однако цимес данного примера в том, что до C++11 если в функцию передавали временный объект как фактический аргумент, а формальный аргумент являлся ссылкой на константу, то компилятор с этим не мог ничего поделать, и производил копирование временного объекта, что "дорого".
Собственно, этот хитрый "бенчмарк" для этого и предназначен (на самом деле надо было в примере передавать не одну и ту же строку в конструктор пары).
Проблема была очень насущной, и приходилось использовать разные ухищрения для устранения ненужного копирования, например COW (Copy on Write) - в строчку добавляли счетчик ссылок, и при копировании строки производилось его увеличение, в деструкторе - уменьшение. Если строчку каким-либо образом меняли, то создавался новый экземпляр строки, счетчик для новой строки приравнивался единице. Как счетчик ссылок равнялся нулю - динамически аллоцированную строчку прибивали.
На 58:00 непонятно, разве компилятор не должен сгенерировать за нас конструкторы и операторы присваивания по умолчанию, то есть мы просто можем опустить объявление этих конструкторов? И зачем тогда default...
Если определить какой-нибудь конструктор вручную, то конструктор по умолчанию, простите за тавтологию, не генерируется по умолчанию. Чтобы указать что его все равно нужно сгенерировать, нужен будет default.
Спасибо за лекции, но я понял 5% )))
5% это хорошее начало )
Формально среди умных указателей есть еще и std::exception_ptr.
1:40:05 а можно уточнить про что это?
Речь идёт о spaceship operator (en.cppreference.com/w/cpp/language/default_comparisons#Defaulted_three-way_comparison). Более подробно останавливались на нём в лекции 12 (th-cam.com/video/KPuYn_fUdxc/w-d-xo.html&ab_channel=PVS-StudioRu). Если коротко - теперь можно написать следующий код:
struct SomeStruct
{
int n; std::string s; float d;
auto operator(const SomeStruct &) const = default;
};
А всё :) Теперь компилятор сможет за вас сгенерировать тела всех операторов сравнения. Кода стало заметно меньше. Если требуется более сложная кастомная логика - просто пишем свою имплементацию.
лайк
49:37 std::greater ?
Да, в том примере для сортировки по убыванию действительно можно воспользоваться функциональным объектом "std::greater", даже до C++11. Однако если потребуется более сложная логика, до C++11 пришлось бы реализовывать кастомный класс с перегруженным "operator()". Скорее всего, вы бы не хотели, чтобы этот класс был виден снаружи вашей функции, т.е. класс бы пришлось делать локальным. C++11 добавил анонимные функции, которые делают все то же самое за вас.
Если копнуть еще глубже, то лямбды имеют преимущество над локальными классами. Например, у последних имеется вот такой список ограничений (en.cppreference.com/w/cpp/language/class#Local_classes). Наиболее существенный из них - невозможность определять внутри класса шаблоны. Обобщенные лямбды из C++14 не имеют такого недостатка, и можно смело использовать (godbolt.org/z/rfbErEYxE) ключевое слово "auto" при объявлении формальных параметров. Объявление хотя бы одного такого формального параметра делает перегруженный оператор внутри лямбды шаблоном.
@@PVSStudioTool Спасибо что потратили время и дали очень развернутый ответ :)
@@sergeyvlasov207 Спасибо, что смотрите наши видео)
Стандарт плюсов неисчерпаем,
матчасть учите, кто не глуп -
и будет вам тогда и страус,
и труп.
Его пример другим наука,
Когда он начал прозревать,
Стандарт открыл, чертей всех вспомнил,
И мать.
Стой, а до 11го лямбд не было?
Были функциональные объекты (предикаты), но это всё нужно было делать руками... Только с 11-тых пришли нормальные лямбды (((
откуда он знает что было изменено в 20 стандарте, если видео выпущено 1 окт 2019, а c++20 в декабре 2020?
Я так понимаю все благодаря boost библиотекам, либо благодаря ранним официальным анонсам || инсайдерской инфе. Что-то вроде того, когда все за полгода до выхода уже знают дизайн следующего айфона)
JVM на С пишется а не на С++, жеж))
Ну мы же понимаем, что выражение TimeKeeper time_keeper(Timer()); не есть определение функции которая возвращает TimeKeeper и принимает на входе другую функцию возвращающую Timer и не принимающую никаких аргументов. Т.е. по сути Timer(*fun)() или TimeKeeper time_keeper(Timer(*fun)()) для пущей убедительности. Вот щаз она точно выглядит как декларация функции с именем time_keeper. Но у этой функции нет метода get_time. У функции я должен сказать вообще нет никаких методов в отличие от TimeKeeper у которого такой метод есть. Вот на это компилятор и ругается. Чего вы там совсем ку-ку вызываете метод у функции? К слову сказать у функционального объекта могут быть методы и поля, а у просто функции нет... Но мы-то вовсе не это хотели написать а определение экземпляра класса TimeKeeper с инициаллизацией которая описана в его конструкторе. Но как это объяснить компилятору если у него в приоритете функциональный подход а потом классовый. Хотя тов Ленин учил, что ко всему нужен классовый подход. И если бы компилятор так сделал как учил наш великий учитель тов Ленин то компилятор бы не запутался в 3-х соснах а сделал бы всё правильно... А не решил что это какая-то непонятная ХРЕНЬ и потом начал на эту ХРЕНЬ ругаться, которой там даже не предполагалось. О как! Ну не всё хорошо делает компилятор, но они над этим работают и пытаются устранить... Пока 23-ая версия пошла. Модули я ещё не пробовал. Но на паскале писал когда-то... Там всё примерно так как сказано в описании. Впрочем я это сразу заметил. на Фортране тоже модули.. И на Питоне с перлом. И только на плюсах хэдера. В связи с этим вопрос, а как dll писать? Ведь там это принципиально важно. И не только там... Если нет хэдеров то как же тогда? Или сделают ещё один формат выполняемых, подключаемых файлов? В паскале это TPU блин... 1994 год глубокое средневековье, а там модули!!! Я всегда говорил, что всё новое это просто хорошо забытое старое. Мы ходим по кругу... Ничего принципиально нового не выдаём... Всё где-то когда-то как-то было.
Ну мы же понимаем, что выражение TimeKeeper time_keeper(Timer()); не есть определение функции которая возвращает TimeKeeper и принимает на входе другую функцию возвращающую Timer и не принимающую никаких аргументов.
Это человек понимает семантически, что здесь декларируется объект, потому что это было его желание. А вот стандарт C++, по которому реализованы компиляторы, здесь не согласен. И дело в тяжком наследии C. C++ исторически создавался как расширение C, поэтому грамматика была переиспользована. И к сожалению, по стандарту здесь предпочитается грамматическое правило по разбору декларации функции, поскольку не встретилось ничего, что могло бы остановить парсинг согласно этому правилу.
Вы можете попробовать изменить это, отправив предложение в стандарт:
stdcpp.ru/proposals
Но успеха здесь ждать не стоит. Ведь C++ развивается с ориентацией на обратную совместимость с языком C. Только создавать другой диалект C++, как например этим занимается Герб Саттер (C++2):
github.com/hsutter/cppfront
В связи с этим вопрос, а как dll писать?
Примерно так же, как и раньше :) Итоговое приложение или динамическая библиотека получается после линкера. Здесь ничего не поменяли. Изменили этап компиляции с применением модулей. Модуль состоит из интерфейсного файла и имплементации. Когда модуль компилируется, компилятор формирует 2 файла: обычный объектный файл для линковки и BMI (Binary Module Interface). Первый всё также передается линкеру, а вот второй использует компилятор, когда встречает import в компилируемом файле. Т.к. модуль не меняется из-за внешних макроопределений (в отличие от заголовочных файлов), это позволяет сериализовать некоторое промежуточное представление в BMI и десериализовывать его, не тратя время на повторный разбор кода.
90% из всего перечисленного писано на С без плюсов.
вроде на си писать умею, но из того что говорит этот дядька не имеет смысла ничего. насколько же я тупой((
Вовсе нет, но теперь вы стали чуть более образованным)