Огромное спасибо за очередную превосходную лекцию. Вы знаете, я по доброму, но завидую вашим студентам. Они, наверное, даже пока не представляют как же им повезло в начале пути попасть на такую качественную подачу материала. То, до чего почти все мы доходили годами, потом и кровью, вы рассказываете, причем с превосходной подачей. Разумеется почти всё это они забудут, но где то на подкорке оно останется и, я уверен, поможет быстрее стать хорошими инженерами.
Обожаю лекции Константина, часто рекомендую начинающим. Хоть вещи рассказываются весьма тривиальные, но, на мой взгляд, это именно то, что дает настоящий драйв и мотивацию изучать языки, их дизайн и философию. Самые интересные вещи в С++ лежат на поверхности. + Константин как лектор и диктор весьма состоялся и нашел себя, слушать его интересно и даже зная о предмете разговора - любопытно.
Здравствуйте, Константин. Большое спасибо за отличную лекцию. Хочу указать на небольшую, на мой взгляд, неточность. На 18 слайде (35:43) от IScreen к IFigure на слайде проведено отношение агрегирования. Однако интерфейс не может выступать в роли «Целого», так не имеет полей-данных. Агрегировать может только класс. Здесь корректнее было бы провести отношение зависимости IScreen от IFigure (пунктирная стрелка)
Ох. Одновременно крайне рад за ваших студентов и сочувствую им. Преподаватель им попался просто замечательный, правда часто рассматривает так подробно, что нужно уже не просто изучать язык, а хоть немного потыкаться в него, т.к. нужно уже хорошо плавать в нём, чтобы следить за вашим ходом мысли без лишних раздумий (тем студентам, которые не делали ни одной HWT к моменту этой лекции- соболезную). Но, что не убивает - делает сильнее! В крайнем случае, потом смогут посмотреть повтор, за что отдельное вам огромное спасибо, но уже от меня, т.к. могу прикоснуться к такому обучающему материалу, не будучи студентом МФТИ. Впрочем, думаю в МФТИ не очень много студентов, которые просто слушают лекции и в свободное время ничего не практикуют. Просто огромаднейшее спасибо вам! Крайне рад слушать эти лекции, и судя по комментариям не я один! Не знаю насколько большой ваш вклад профессиональный, но в преподавании - 100% точно большой! Обычно говорят, что если из тебя не получается хорошего специалиста - иди учить; даже учитывая, что мало знаю вас, этот "совет" явно к вам не относится! В прицнипе, обычно не смотрю на других, только сам на себя, но на вас прям так и хочется ровняться. Особенно очень ценно, что несмотря на вашу явную профессиональность - видно, что всё равно знаете куда расти, к чему стремиться! Это тоже восхищает! А так же непомерно благодарен отсальным, кто сделал возможным просмотр этих лекций; кто разрешил их выкладывать на UA-cam, кто помогал с лекциями, и т.п..
1:04:00 строка 79, вы объявили draw у Drawable как friend-функцию, а не метод класса. Это сделано для унификации с другими функциями draw чтобы можно было наворачивать рекурсивные структуры типа DrawableObject или по каким-то другим причинам? Просто из других ваших лекций сложилось представление, что вы не рекомендуете использовать friend'ы.
Спасибо за ответ и за лекцию конечно. Мощная штука тогда получилась, хотя я долго думал и не смог понять зачем может понадобиться строить DrawableObject если что угодно, для чего есть функция draw, можно сразу обернуть в DrawableObject.
Большое спасибо за интересную и полезную лекцию . У меня вопрос - планируются ли ещё лекции в плане проектирования ПО , а конкретно объединение классов в пакеты , критерии «хорошего» пакета и зависимости между пакетами . Спасибо
Спасибо. То что вы предлагаете это не базовый уровень, т.к. понятие пакета в языке не определено. Это что то вроде допсеминара для магистрантов. Очень специфичного. Я подумаю, конечно, но я не уверен, что я сам тут компетентен.
Спасибо большое за лекцию, очень интересно было смотреть. Пример с Parent Reversal - это вообще красота. На 24-м слайде вы говорили, что есть другие техники совмещения SRP и OCP. Не могли бы вы подсказать, где можно поискать другие методики выполнения обоих принципов?
Не совсем понятно по поводу функции IM& IM::clone(const IM&) 4:06 . Параметр же инвариантен, только возвращающее значение контрвариантно, какая реализация может сохранить нас от не правильного копирования, на ум только реализация с dynamic_cast приходит?
Она вызывается у объекта наследника который знает как себя копировать. Другое дело что ей вообще-то не нужен параметр. Ну или можно сделать её статической. Виртуальная функция clone с параметром это опечатка.
В примере DIP нет инверсии зависимости как таковой, т.е. направление зависимостей в примерах осталось неизменным. Если у нас есть некий модуль, реализующий "высокоуровневую логику" и есть "низкоуровневая система ввода-вывода", то система ввода-вывода предоставляет интерфейс для выполнения этого самого ввода-вывода. И это еще одна паразитная зависимость - высокоуровневая логика зависит от интерфейса системы ввода-вывода, т.е. зависимость направлена от высокого уровня к низкому. А вот если в нашем модуле логики будет интерфейс ввода-вывода, а система ввода-вывода будет реализовывать данный интерфейс, то зависимость будет идти от низкого уровня к высокому, т.е. мы инвертировали её, тем самым модуль логики стал более самодостаточен. Если не ошибаюсь, подобный пример и в книге Мартина приводится.
В моём примере всё обстоит именно так. Планировщик зависел от конкретной реализации базового блока. Мы сделали интерфейс и теперь базовый блок реализует этот интерфейс. Вроде один в один. Я что-то упускаю?
@@tilir наверное, я не очень понятно изложил что имею ввиду (всё-таки Вы прекрасный лектор, а я среднестатистический "эксперт"). Набросал диаграмму, которая, надеюсь, решит наше недопонимание, но злой youtube не пускает ссылку в комментарии, поэтому только так: diskТОЧКАyandexТОЧКАru/i/bD3vfr3yqoV_8Q
Спасибо, это отличная трактовка Мартина. Продублирую ссылку (надеюсь меня не удалит): disk.yandex.ru/i/bD3vfr3yqoV_8Q Отношения те же самые, но другая физическая компоновка модулей. У меня в лекции не было про физическую компоновку, только про отношения между классами. Но методически это прекрасное объяснение почему dependency inversion это именно inversion. Я, пожалуй, скорректирую своё изложение в следующем году с учётом этого.
На самом деле в примере в лекции наверное не хватает какой-то пунктирной линии, отделяющей один модуль от другого, с ней все станет на свои места. Scheduler и IDAG это один модуль, DAG и IBB - другой, а конкретные BB - третий. Scheduler говорит, мол умею работать с DAG, который реализует IDAG, Конкретный DAG говорит, что он работает с IBB, нижележащий слой должен реализовать, т.е. зависеть от этого IBB. В реальном LLVM используется похожий подход для реализации CFG, только не через интерфейсы, а через специализации шаблонов (чтобы построить CFG из сепулек надо специализировать некий шаблон для сепульки). Впрочем было предложение и бурно обсуждалось переписать на ООП с динамическим полиморфизмом, не помню уже аргументацию, но проблема в том, что слишком много CFG в проекте: для IR - один, для MachineIR - другой, а ещё MLIR есть...
+, в примерах важно выделять границы модулей (например, как сказано выше, пунктирной линией). Т.е. интерфейс - это часть модуля верхнего уровня, и получается, что модуль верхнего уровня зависит сам от себя, а не от модуля нижнего уровня. Весь проект может быть построен на интерфейсах, но при этом в нем не будет ни одной инверсии зависимостей, если они располагаются/принадлежат не тому модулю, поэтому на это (принадлежность интерфейса модулю верхнего уровня) нужно также обращать внимание
Множество интерфейсов это наш Java путь. В С++ шаблоны позволяют реализовать duck typing и обойтись без общих интерфейсов. Хотим реализовать поиск по массиву или коллекции - не нужно никаких ": public Iterable", достаточно чтобы в коллекции были объявлены два метода: begin() и end(). Выигрыш - производительность, т.к. нет виртуальных вызовов. Увы за это приходится платить дикими, зачастую нечитабельными сообщениями об ошибках компиляции либо обмазыванием кода enable_if/static_assert, либо (с С++20) дисциплиной в прописывании концептов. Ну и худшей поддержкой в различных IDE, т.к. пока шаблон не инстанцировался, то непонятно есть нужные методы у его параметров или нет.
Про особенности шаблонной магии в следующем семестре. В этой лекции мы подытоживали классическое ООП, а оно в общем на всех одно. Если вы заметили, у меня никаких public Iterable и не было, когда мне были нужны неявные интерфейсы я неявными и пользовался. Но динамический полиморфизм увы безальтернативен -- как только вам нужно сложить в одну коробку много разных игрушек, он так или иначе появляется.
Про Iterable, я имел ввиду как оно было бы без шаблонов и как сейчас есть в Java и других языках, хотя тот же Rust позволяет вызывать методы класса (структуры) - параметра без виртуальных вызовов за счёт мономорфизации, но и виртуальная диспетчеризация никуда не делась, можно пользоваться при желании. При этом компилятор проверит, что переданный класс реализует требуемый интерфейс (трейт) Т и обойти эту проверку никак нельзя. По поводу коробочек с игрушками, есть идея заводить по коробочке на каждый тип игрушки, тогда по ним можно будет итерироваться и что-то делать без виртуальных вызовов и наши кеши останутся мягкими, шелковистыми и прогретыми. Да впрочем понимаю, что вы и сами это все знаете, эта лекция про классический ООП, но классическому ООП постоянно кто-то наступает на пятки.
@@tilir Девиртуализация и инлайнинг выглядят как оптимизации, могут произойти, а могут, если нет данных о конкретном типе, - не произойти. Я наверное заоффтопил, но привел мономорфизацию как один из механизмов реализации шаблонов/дженериков. В Java, например, все вызовы виртуальные поэтому в общем случае можем просто очистить тип нашего параметра Т после компиляции (по историческим причинам так и сделали). В Rust мы поступаем похожим на С++ способом (как я это понимаю): компилируем шаблонный метод для каждого Т отдельно в какой-нибудь foo и отдельно foo. И там компилятор требует если в шаблоне используешь какой-нибудь вызов T.method(), то будь добр укажи, что T реализует какой-нибудь интерфейс, содержащий метод method(), без этого не скомпилирует, отвалится на этапе семантического анализа. А в С++ мы просто натурально заменяем Т на нужный тип на этапе переписывания AST (как в Rust не знаю, по моему мономорфизация производится позже уже на этапе обработки или генерации MIR). Так как мы просто переписываем AST, то нам достаточно просто указать template , никаких ограничений, что такое T указывать не нужно, есть там нужные поля или методы - гуд, нет - получи, программист, ошибку и это твоя забота получить ее как можно раньше. Мне подход Rust нравится больше, хотя плюсовый гибче, но никак не покидает мысль, что пишешь как на Python каком-нибудь.
@@tilir а вообще вы правы, от языка здесь только то, что язык Rust говорит: "я тут обязательно девиртуализую, а если тебе не надо, то скажи мне об этом".
Слайд 4: virtual ~IM() = 0; должно быть, думаю, default. Подзреваю, просто опечатка (ну или трюк, если clone почему-то намерено не сделана чисто вирутальной, и чистую виртуальность обеспечивает деструктор, для которого, тем не менее, где-то есть реализация). Плюс не очень понятно, зачем в clone передавать еще одну матрицу (и что будет, если он будет не того типа). Но это так, придирки, в целом то идея ясна. А вот... Слайды 27-29: Грусть-печаль. Автор, по всему очень знающий специалист и замечательный лектор, кажется, наступил на... одну из классических мин C++. В функцию draw, принимаюущую указатель на массов Shape, НЕЛЬЗЯ передавать указатель на массив Rectangle. Чтоб понять почему, нужно написать любую разумную реализацию этой функции, где, видимо, будет код типа shapes[i], и подумать над тем, что будет происходить, если Rectangle добавил относительно Shape новое поле и размер наследника больше, чем у Shape. Я совсем не эксперт, но подозреваю, что корень того, почему так делать нельзя, лежит как раз в плоскости размышлений, включающих слово "ковариантность" (которой и посвящены слайды). Только указатели на одинарные объекты в плюсах корректно преобразовывать по иерархии. P.S. Смотрю магистерский курс - лекции в целом правда отличные.
@@tilir Я может быть не совсем понял вопрос. Если вы обращаетесь только к первому элементу массива, то, разумеется, все будет хорошо. Как при обращении к членам базового класса, так и к вирутальным/не виртуальным функциям. Проблема же возникает именно при итерировании: shapes[i] должно знать на сколько сместиться и он смещает на размер базового класса * i, если по указателю реально массив наследников и у наследника размер другой, то shapes[1] дает указатель где-то посередине первого (по индексу 0) rectangle в массиве. Попытка позвать на нем хоть что-нибудь должна закончиться печально и в общем случае непредсказуемо godbolt.org/z/8qfW31MMs
Интересный консерн. Спасибо я об этом действительно не подумал. Я говорил о синтаксической возможности так сделать. Кажется я знаю как улучшить эти слайды: рекомендацией так не делать ))
Здравствуйте, у Вас в коде по SRP (~30мин) указаны не константные итераторы begin и end, хотя в цикл передаётся константная ссылка, подскажите как можно заставить цикл для диапазонов брать cbegin и cend?
Увы в [stmt.ranged] есть только три варианта: (1) range и range+N (2) range.begin() и range.end() (3) std::begin(range) и std::end(range) Так что правильный ответ -- насколько я понимаю, никак. Но есть хорошая новость. В требованиях к стандартных контейнерам указано, что begin() const и cbegin() должны вести себя идентично см. [tab:container.req] То есть вы можете просто сделать begin() const возвращающим const iterator (вы ОБЯЗАНЫ его таким сделать) и дальше итерироваться по константному диапазону типа for(auto Elt: std::as_const(Cont)) и т.п. Ну и как вы сами понимаете я это и сделал =) Я не останавливался на деталях т.к. уже очень скоро будет лекция по контейнерам и итераторам где я расскажу всё это и многое другое.
Никак не получалось на словах понять принцип инверсии зависимостей - в чем заключается инверсия? Пока не нарисовал для себя картинку: Объекты высокого уровня (бизнес-логика) .---> Интерфейсы
Подскажите, пожалуйста, какой программой Вы пользуетесь при написании UML диаграмм? Нашел только Dia и MS Visio, первый не такой красивый как у Вас, а второй платный.
МакКоннел - 93й год, Банда Четырех - 94й год, как они могли быть написаны с упором на Java, если Java официально вышла только в 95м? Понятно, что эта технология какое-то время разрабатывалась в недрах Sun Microsystems, но не уверен, что авторы этих книг про нее знали да ещё и в деталях. Скорее это С++ тогда такой был, шаблоны появились в 91-м году, STL в 92-93. Книгу МакКоннела читал давно и явно не первое издание, Вики пишет, что в первом издании примеры были на С, Basic и Pascal. Вообще наверное такая эволюция языка С++, особенно начиная с 11го стандарта, свидетельствует, что знать о паттернах надо, но заучивать их реализацию не стоит, завтра в язык завезут новые возможности (те же лямбды или pattern matching) и какие-то паттерны отомрут, а другие станут реализовываться в одну строчку. Паттерны это скорее наш программистский фольклор или часть языка общения. Но на собеседованиях все ещё спрашивают хотя бы названия (натурально, перечислите все паттерны проектирования, которые знаете).
Спасибо за лекцию. Отличная презентация от Шона Парента по одной из затронутых тем: ua-cam.com/video/QGcVXgEVMJg/v-deo.html (полиморфизм без наследования, “runtime concept idiom”).
Не раскрыта тема противоречия хорошей архитектуры и производительности, скажем в первом случае появляется копирование, а с интерфейсами разбитыми на N виртуальных интерфейсов страшно смотреть на sizeof P.S. объяснение про зависимости от интерфейсов лучше на шаблонах наверное крутить, т.к. ... Ну лучше так. Например более менее одинаковые интерфейсы контейнеров стандартных, а использующий их код становится как бы шаблонным, т.к. можно подменить один контейнер на другой и код скомпилируется(если "удовлетворял интерфейсу" например ассоциативного контейнера) Короче говоря в некотором роде стандартная либа стимулирует к написанию хорошего кода и делает сложным написание плохого кода, важный момент P.P.S из getpath хотелось бы возвращать std::filesystem::path ... Что как бы примерно того же уровня абстракции что и директория
У вас такой, российский подход к тому, как выразить свое мнение по поводу работы другого человека. Надо несколько иначе, вот так, например - "Константин, большое спасибо за отличную лекцию. Мне особенно понравился слайд 38, где вы говорите о необходимости быть вежливым, в целом - доброжелательным, при обсуджении работы другого человека. Не могли бы вы еще затронуть вопросы производительности в связке с принципами проектирования? Было бы интересно познакомиться с вашим опытом. Еще раз спасибо".
@@andreysolovyev630 это провоцирует плохое проектирование человеческих отношений, лучше подразумевать, иметь договор, что люди пишут тем, от кого хотят получить ответ в том или ином виде, например в виде улучшения качества контента, а если хотят улучшить качество, это не значит что оно плохое, просто хотят развития мира в сторону улучшения
Мне кажется, хорошая архитектура всегда принимает в расчёт производительность. Противоречие есть между производительностью и слепым выполнением абстрактных принципов, это да =) Про стандартную библиотеку будет подробно в следующем семестре, здесь мы подытоживаем классику ООП. Про std::filesystem я вроде упомянул...
Огромное спасибо за очередную превосходную лекцию.
Вы знаете, я по доброму, но завидую вашим студентам. Они, наверное, даже пока не представляют как же им повезло в начале пути попасть на такую качественную подачу материала.
То, до чего почти все мы доходили годами, потом и кровью, вы рассказываете, причем с превосходной подачей.
Разумеется почти всё это они забудут, но где то на подкорке оно останется и, я уверен, поможет быстрее стать хорошими инженерами.
Обожаю лекции Константина, часто рекомендую начинающим. Хоть вещи рассказываются весьма тривиальные, но, на мой взгляд, это именно то, что дает настоящий драйв и мотивацию изучать языки, их дизайн и философию. Самые интересные вещи в С++ лежат на поверхности.
+ Константин как лектор и диктор весьма состоялся и нашел себя, слушать его интересно и даже зная о предмете разговора - любопытно.
Здравствуйте, Константин. Большое спасибо за отличную лекцию. Хочу указать на небольшую, на мой взгляд, неточность. На 18 слайде (35:43) от IScreen к IFigure на слайде проведено отношение агрегирования. Однако интерфейс не может выступать в роли «Целого», так не имеет полей-данных. Агрегировать может только класс. Здесь корректнее было бы провести отношение зависимости IScreen от IFigure (пунктирная стрелка)
Ох. Одновременно крайне рад за ваших студентов и сочувствую им. Преподаватель им попался просто замечательный, правда часто рассматривает так подробно, что нужно уже не просто изучать язык, а хоть немного потыкаться в него, т.к. нужно уже хорошо плавать в нём, чтобы следить за вашим ходом мысли без лишних раздумий (тем студентам, которые не делали ни одной HWT к моменту этой лекции- соболезную). Но, что не убивает - делает сильнее! В крайнем случае, потом смогут посмотреть повтор, за что отдельное вам огромное спасибо, но уже от меня, т.к. могу прикоснуться к такому обучающему материалу, не будучи студентом МФТИ. Впрочем, думаю в МФТИ не очень много студентов, которые просто слушают лекции и в свободное время ничего не практикуют.
Просто огромаднейшее спасибо вам! Крайне рад слушать эти лекции, и судя по комментариям не я один! Не знаю насколько большой ваш вклад профессиональный, но в преподавании - 100% точно большой! Обычно говорят, что если из тебя не получается хорошего специалиста - иди учить; даже учитывая, что мало знаю вас, этот "совет" явно к вам не относится!
В прицнипе, обычно не смотрю на других, только сам на себя, но на вас прям так и хочется ровняться. Особенно очень ценно, что несмотря на вашу явную профессиональность - видно, что всё равно знаете куда расти, к чему стремиться! Это тоже восхищает!
А так же непомерно благодарен отсальным, кто сделал возможным просмотр этих лекций; кто разрешил их выкладывать на UA-cam, кто помогал с лекциями, и т.п..
1:19:52 начал сомневаться - interface segregation или interface separation, вроде бы правильно segregation
Внезапно действительно segregation. То есть это не я ошибался, это я правильно помнил на автомате, но почему-то неправильно записал для слайдов =)
Спасибо огромное за лекцию. Подскажите, на ютубе есть ваша лекция по паттернам проектирования?
Пока нет, но рано или поздно...
Комментарий для себя, для закладки (с Хабра): 01:00:28
1:04:00 строка 79, вы объявили draw у Drawable как friend-функцию, а не метод класса. Это сделано для унификации с другими функциями draw чтобы можно было наворачивать рекурсивные структуры типа DrawableObject или по каким-то другим причинам? Просто из других ваших лекций сложилось представление, что вы не рекомендуете использовать friend'ы.
В данном случае это цитата из Шона Парента -- я постарался оставить как можно более близкий код к его примеру. Но я полагаю, что да, для унификации.
Спасибо за ответ и за лекцию конечно. Мощная штука тогда получилась, хотя я долго думал и не смог понять зачем может понадобиться строить DrawableObject если что угодно, для чего есть функция draw, можно сразу обернуть в DrawableObject.
Если честно, то я не понял зачем там friend (class Drawable в районе 01:00:00)
Большое спасибо за интересную и полезную лекцию . У меня вопрос - планируются ли ещё лекции в плане проектирования ПО , а конкретно объединение классов в пакеты , критерии «хорошего» пакета и зависимости между пакетами . Спасибо
Спасибо. То что вы предлагаете это не базовый уровень, т.к. понятие пакета в языке не определено. Это что то вроде допсеминара для магистрантов. Очень специфичного. Я подумаю, конечно, но я не уверен, что я сам тут компетентен.
Про физический дизайн хорошо пишет и рассказывает Лакос, Large-Scale C++ Software Design (интересно, пропустит ли Гугл коммент в этот раз).
1:00:32 Конструктор +Drawable(x:int) оставлен по ошибке?
Спасибо большое за лекцию, очень интересно было смотреть. Пример с Parent Reversal - это вообще красота. На 24-м слайде вы говорили, что есть другие техники совмещения SRP и OCP. Не могли бы вы подсказать, где можно поискать другие методики выполнения обоих принципов?
Есть отличный доклад Иглебергера, упомнятый мной в т.ч. в литературе: ua-cam.com/video/Ntraj80qN2k/v-deo.html
Оттуда дальше можно идти по ссылкам.
@@tilir Спасибо большое!
Parent Reversal вроде как = visitor
Не совсем понятно по поводу функции IM& IM::clone(const IM&) 4:06 . Параметр же инвариантен, только возвращающее значение контрвариантно, какая реализация может сохранить нас от не правильного копирования, на ум только реализация с dynamic_cast приходит?
Она вызывается у объекта наследника который знает как себя копировать. Другое дело что ей вообще-то не нужен параметр. Ну или можно сделать её статической. Виртуальная функция clone с параметром это опечатка.
В примере DIP нет инверсии зависимости как таковой, т.е. направление зависимостей в примерах осталось неизменным.
Если у нас есть некий модуль, реализующий "высокоуровневую логику" и есть "низкоуровневая система ввода-вывода", то система ввода-вывода предоставляет интерфейс для выполнения этого самого ввода-вывода. И это еще одна паразитная зависимость - высокоуровневая логика зависит от интерфейса системы ввода-вывода, т.е. зависимость направлена от высокого уровня к низкому. А вот если в нашем модуле логики будет интерфейс ввода-вывода, а система ввода-вывода будет реализовывать данный интерфейс, то зависимость будет идти от низкого уровня к высокому, т.е. мы инвертировали её, тем самым модуль логики стал более самодостаточен. Если не ошибаюсь, подобный пример и в книге Мартина приводится.
В моём примере всё обстоит именно так. Планировщик зависел от конкретной реализации базового блока. Мы сделали интерфейс и теперь базовый блок реализует этот интерфейс. Вроде один в один. Я что-то упускаю?
@@tilir наверное, я не очень понятно изложил что имею ввиду (всё-таки Вы прекрасный лектор, а я среднестатистический "эксперт"). Набросал диаграмму, которая, надеюсь, решит наше недопонимание, но злой youtube не пускает ссылку в комментарии, поэтому только так: diskТОЧКАyandexТОЧКАru/i/bD3vfr3yqoV_8Q
Спасибо, это отличная трактовка Мартина. Продублирую ссылку (надеюсь меня не удалит): disk.yandex.ru/i/bD3vfr3yqoV_8Q
Отношения те же самые, но другая физическая компоновка модулей. У меня в лекции не было про физическую компоновку, только про отношения между классами. Но методически это прекрасное объяснение почему dependency inversion это именно inversion. Я, пожалуй, скорректирую своё изложение в следующем году с учётом этого.
На самом деле в примере в лекции наверное не хватает какой-то пунктирной линии, отделяющей один модуль от другого, с ней все станет на свои места. Scheduler и IDAG это один модуль, DAG и IBB - другой, а конкретные BB - третий. Scheduler говорит, мол умею работать с DAG, который реализует IDAG, Конкретный DAG говорит, что он работает с IBB, нижележащий слой должен реализовать, т.е. зависеть от этого IBB.
В реальном LLVM используется похожий подход для реализации CFG, только не через интерфейсы, а через специализации шаблонов (чтобы построить CFG из сепулек надо специализировать некий шаблон для сепульки). Впрочем было предложение и бурно обсуждалось переписать на ООП с динамическим полиморфизмом, не помню уже аргументацию, но проблема в том, что слишком много CFG в проекте: для IR - один, для MachineIR - другой, а ещё MLIR есть...
+, в примерах важно выделять границы модулей (например, как сказано выше, пунктирной линией).
Т.е. интерфейс - это часть модуля верхнего уровня, и получается, что модуль верхнего уровня зависит сам от себя, а не от модуля нижнего уровня.
Весь проект может быть построен на интерфейсах, но при этом в нем не будет ни одной инверсии зависимостей, если они располагаются/принадлежат не тому модулю, поэтому на это (принадлежность интерфейса модулю верхнего уровня) нужно также обращать внимание
Спасибо за лекцию очень интересно. По паттернам по моему скромному мнению лучше прочитать Эрика Фримена.
Эта книга мне всегда казалась немножко слишком заточенной на джаву. И не слишком серьёзной.
Множество интерфейсов это наш Java путь. В С++ шаблоны позволяют реализовать duck typing и обойтись без общих интерфейсов. Хотим реализовать поиск по массиву или коллекции - не нужно никаких ": public Iterable", достаточно чтобы в коллекции были объявлены два метода: begin() и end(). Выигрыш - производительность, т.к. нет виртуальных вызовов. Увы за это приходится платить дикими, зачастую нечитабельными сообщениями об ошибках компиляции либо обмазыванием кода enable_if/static_assert, либо (с С++20) дисциплиной в прописывании концептов. Ну и худшей поддержкой в различных IDE, т.к. пока шаблон не инстанцировался, то непонятно есть нужные методы у его параметров или нет.
Про особенности шаблонной магии в следующем семестре. В этой лекции мы подытоживали классическое ООП, а оно в общем на всех одно. Если вы заметили, у меня никаких public Iterable и не было, когда мне были нужны неявные интерфейсы я неявными и пользовался. Но динамический полиморфизм увы безальтернативен -- как только вам нужно сложить в одну коробку много разных игрушек, он так или иначе появляется.
Про Iterable, я имел ввиду как оно было бы без шаблонов и как сейчас есть в Java и других языках, хотя тот же Rust позволяет вызывать методы класса (структуры) - параметра без виртуальных вызовов за счёт мономорфизации, но и виртуальная диспетчеризация никуда не делась, можно пользоваться при желании. При этом компилятор проверит, что переданный класс реализует требуемый интерфейс (трейт) Т и обойти эту проверку никак нельзя. По поводу коробочек с игрушками, есть идея заводить по коробочке на каждый тип игрушки, тогда по ним можно будет итерироваться и что-то делать без виртуальных вызовов и наши кеши останутся мягкими, шелковистыми и прогретыми. Да впрочем понимаю, что вы и сами это все знаете, эта лекция про классический ООП, но классическому ООП постоянно кто-то наступает на пятки.
Мне кажется "мономорфизация" это не более чем девиртуализация плюс инлайн. То есть это не о языке, это про оптимизации компилятора.
@@tilir Девиртуализация и инлайнинг выглядят как оптимизации, могут произойти, а могут, если нет данных о конкретном типе, - не произойти. Я наверное заоффтопил, но привел мономорфизацию как один из механизмов реализации шаблонов/дженериков. В Java, например, все вызовы виртуальные поэтому в общем случае можем просто очистить тип нашего параметра Т после компиляции (по историческим причинам так и сделали). В Rust мы поступаем похожим на С++ способом (как я это понимаю): компилируем шаблонный метод для каждого Т отдельно в какой-нибудь foo и отдельно foo. И там компилятор требует если в шаблоне используешь какой-нибудь вызов T.method(), то будь добр укажи, что T реализует какой-нибудь интерфейс, содержащий метод method(), без этого не скомпилирует, отвалится на этапе семантического анализа. А в С++ мы просто натурально заменяем Т на нужный тип на этапе переписывания AST (как в Rust не знаю, по моему мономорфизация производится позже уже на этапе обработки или генерации MIR). Так как мы просто переписываем AST, то нам достаточно просто указать template , никаких ограничений, что такое T указывать не нужно, есть там нужные поля или методы - гуд, нет - получи, программист, ошибку и это твоя забота получить ее как можно раньше. Мне подход Rust нравится больше, хотя плюсовый гибче, но никак не покидает мысль, что пишешь как на Python каком-нибудь.
@@tilir а вообще вы правы, от языка здесь только то, что язык Rust говорит: "я тут обязательно девиртуализую, а если тебе не надо, то скажи мне об этом".
Здравствуйте, на 45:57 упоминается доклад Eagleburger 2017 никак не получается его нагуглить, есть ли ссылка на этот доклад?
Klaus Igleberger
@@tilir Спасибо большое
А какие темы будут в следующем семестре.?
Я написал пост про это на вкладке сообщества: ua-cam.com/users/postUgkxZb0KXNeVaGkYhTvh_kqFArV9WMJEEaqf
Слайд 4:
virtual ~IM() = 0; должно быть, думаю, default. Подзреваю, просто опечатка (ну или трюк, если clone почему-то намерено не сделана чисто вирутальной, и чистую виртуальность обеспечивает деструктор, для которого, тем не менее, где-то есть реализация). Плюс не очень понятно, зачем в clone передавать еще одну матрицу (и что будет, если он будет не того типа). Но это так, придирки, в целом то идея ясна. А вот...
Слайды 27-29:
Грусть-печаль. Автор, по всему очень знающий специалист и замечательный лектор, кажется, наступил на... одну из классических мин C++.
В функцию draw, принимаюущую указатель на массов Shape, НЕЛЬЗЯ передавать указатель на массив Rectangle. Чтоб понять почему, нужно написать любую разумную реализацию этой функции, где, видимо, будет код типа shapes[i], и подумать над тем, что будет происходить, если Rectangle добавил относительно Shape новое поле и размер наследника больше, чем у Shape.
Я совсем не эксперт, но подозреваю, что корень того, почему так делать нельзя, лежит как раз в плоскости размышлений, включающих слово "ковариантность" (которой и посвящены слайды).
Только указатели на одинарные объекты в плюсах корректно преобразовывать по иерархии.
P.S. Смотрю магистерский курс - лекции в целом правда отличные.
Если не секрет, а что будет, если я вызываю через указатели и функции вириуальные?
@@tilir Я может быть не совсем понял вопрос. Если вы обращаетесь только к первому элементу массива, то, разумеется, все будет хорошо. Как при обращении к членам базового класса, так и к вирутальным/не виртуальным функциям. Проблема же возникает именно при итерировании: shapes[i] должно знать на сколько сместиться и он смещает на размер базового класса * i, если по указателю реально массив наследников и у наследника размер другой, то shapes[1] дает указатель где-то посередине первого (по индексу 0) rectangle в массиве. Попытка позвать на нем хоть что-нибудь должна закончиться печально и в общем случае непредсказуемо godbolt.org/z/8qfW31MMs
Интересный консерн. Спасибо я об этом действительно не подумал. Я говорил о синтаксической возможности так сделать. Кажется я знаю как улучшить эти слайды: рекомендацией так не делать ))
Здравствуйте, у Вас в коде по SRP (~30мин) указаны не константные итераторы begin и end, хотя в цикл передаётся константная ссылка, подскажите как можно заставить цикл для диапазонов брать cbegin и cend?
Увы в [stmt.ranged] есть только три варианта:
(1) range и range+N
(2) range.begin() и range.end()
(3) std::begin(range) и std::end(range)
Так что правильный ответ -- насколько я понимаю, никак.
Но есть хорошая новость. В требованиях к стандартных контейнерам указано, что begin() const и cbegin() должны вести себя идентично см. [tab:container.req]
То есть вы можете просто сделать begin() const возвращающим const iterator (вы ОБЯЗАНЫ его таким сделать) и дальше итерироваться по константному диапазону типа for(auto Elt: std::as_const(Cont)) и т.п.
Ну и как вы сами понимаете я это и сделал =)
Я не останавливался на деталях т.к. уже очень скоро будет лекция по контейнерам и итераторам где я расскажу всё это и многое другое.
@@tilir Большое спасибо за ответ, лекции как всегда отличные. С нетерпением жду продолжение
Никак не получалось на словах понять принцип инверсии зависимостей - в чем заключается инверсия? Пока не нарисовал для себя картинку:
Объекты высокого уровня (бизнес-логика) .---> Интерфейсы
Подскажите, пожалуйста, какой программой Вы пользуетесь при написании UML диаграмм? Нашел только Dia и MS Visio, первый не такой красивый как у Вас, а второй платный.
Я пользуюсь dia. До красивого он настраивается ))
МакКоннел - 93й год, Банда Четырех - 94й год, как они могли быть написаны с упором на Java, если Java официально вышла только в 95м? Понятно, что эта технология какое-то время разрабатывалась в недрах Sun Microsystems, но не уверен, что авторы этих книг про нее знали да ещё и в деталях. Скорее это С++ тогда такой был, шаблоны появились в 91-м году, STL в 92-93. Книгу МакКоннела читал давно и явно не первое издание, Вики пишет, что в первом издании примеры были на С, Basic и Pascal. Вообще наверное такая эволюция языка С++, особенно начиная с 11го стандарта, свидетельствует, что знать о паттернах надо, но заучивать их реализацию не стоит, завтра в язык завезут новые возможности (те же лямбды или pattern matching) и какие-то паттерны отомрут, а другие станут реализовываться в одну строчку. Паттерны это скорее наш программистский фольклор или часть языка общения. Но на собеседованиях все ещё спрашивают хотя бы названия (натурально, перечислите все паттерны проектирования, которые знаете).
Я про классическое второе издание (2003 кажется) которое было существенно обновлено и там примеры либо на Java либо на таком C++ который тоже Java.
@@tilir Тогда соглашусь, просто в списке литературы вы ссылались на первое издание, поэтому и удивился.
Спасибо за лекцию.
Отличная презентация от Шона Парента по одной из затронутых тем: ua-cam.com/video/QGcVXgEVMJg/v-deo.html (полиморфизм без наследования, “runtime concept idiom”).
А вернее даже лучше так: “type erasure.”
Не раскрыта тема противоречия хорошей архитектуры и производительности, скажем в первом случае появляется копирование, а с интерфейсами разбитыми на N виртуальных интерфейсов страшно смотреть на sizeof
P.S. объяснение про зависимости от интерфейсов лучше на шаблонах наверное крутить, т.к. ... Ну лучше так.
Например более менее одинаковые интерфейсы контейнеров стандартных, а использующий их код становится как бы шаблонным, т.к. можно подменить один контейнер на другой и код скомпилируется(если "удовлетворял интерфейсу" например ассоциативного контейнера)
Короче говоря в некотором роде стандартная либа стимулирует к написанию хорошего кода и делает сложным написание плохого кода, важный момент
P.P.S из getpath хотелось бы возвращать std::filesystem::path ... Что как бы примерно того же уровня абстракции что и директория
У вас такой, российский подход к тому, как выразить свое мнение по поводу работы другого человека. Надо несколько иначе, вот так, например - "Константин, большое спасибо за отличную лекцию. Мне особенно понравился слайд 38, где вы говорите о необходимости быть вежливым, в целом - доброжелательным, при обсуджении работы другого человека. Не могли бы вы еще затронуть вопросы производительности в связке с принципами проектирования? Было бы интересно познакомиться с вашим опытом. Еще раз спасибо".
@@andreysolovyev630 это провоцирует плохое проектирование человеческих отношений, лучше подразумевать, иметь договор, что люди пишут тем, от кого хотят получить ответ в том или ином виде, например в виде улучшения качества контента, а если хотят улучшить качество, это не значит что оно плохое, просто хотят развития мира в сторону улучшения
@@andreysolovyev630 это шутка?
Мне кажется, хорошая архитектура всегда принимает в расчёт производительность. Противоречие есть между производительностью и слепым выполнением абстрактных принципов, это да =)
Про стандартную библиотеку будет подробно в следующем семестре, здесь мы подытоживаем классику ООП.
Про std::filesystem я вроде упомянул...
У меня не такие высокие стандарты, вопрос ув. Ника Келбона был сформлирован вполне корректно имхо. Но рад, что вам понравился слайд 38 =)
Константин Владимиров крутится - знания мутятся