Comments 82
"...логический use-after-free..." - простое правило, не обращайся к объекту в moved-from стейте (даже если очень хочется, и есть сто кейсов когда нужно и т.д и т.п.). Можно не использовать move вообще. Вообще можно не использовать вещи которые не нужны, а маргинального говна в c++ полно, хоть тоже виртуальное наследование.
"Я не умею запоминать, я умею понимать" - это норм, причём в отношении любого языка. Стоит понимать что к формированию C++ и его библиотек и компиляторов причясны тысячи человек на протяжении десятков лет, естествено что один человек не способен осознать весь этот корпус знаний.
То что автор назвал логическим use after free это буквально относится к любому обращению к переменной после её изменения. Например после swap или просто вы сделали clear на векторе и потом обратились к нему.
В общем претензия самая нелепая из всех что представители языка ада2 предъявляли С++
Я в параллельной ветке привел пример, из которого прямо следует, что графы исполнения для физического и логического use-after-free изоморфны, а, значит, если вы признаете существование физического use-after-free, то следеут уже признать и существование его логического эквивалента в C++.
Я понял правильную аргументацию: возьмите ваш пример с некорректной очисткой вектора и перенесите его в любой другой язык программирования. Ошибка кодирования сохранится, алгоритм будет работать некорректно. А теперь попробуйте сделать то же самое с предложенным мной ниже примером с перемещением.
В этом и есть разница: семантика перемещения - это артефакт отдельного языка, служащий "костылем" для повышения производительности, который сам по себе был рождён в результате неверно принятых концептуальных решений предыдущих этапов.
У меня однажды было так:
метод_1 на тысячу строк перемещает один из своих аргументов в метод_2 где-то в середине своего тела. Мне понадобился доступ к этому аргументу сильно ниже в методе_1. Хорошо, что я имею годами выработанную привычку обмазывать все неизвестные мне вещи ассертами. Я поймал ошибку на CI.
Я даже знаю, как мы к этому пришли: когда-то метод_1 был небольшим, а в методе_2 нужно было изменять переданный аргумент, поэтому его принимали по значению. Логичным решением было переместить имеющийся в методе_1 аргумент. Потом метод_1 отрос подробностями, но код продолжал работать. Пока не пришел тот, кому нужно было этот аргумент зачем-то использовать.
А теперь я вам расскажу два альтернативных сценария:
Вместо одиночного объекта в метод_1 приходит коллекция. Искушение переместить ее значительно возрастает. Но с коллекцией проблема серьезней: очень часто пустая коллекция - это вполне норма. Поди разберись, нужно ли вставлять в коде где ни попадя assert(!vec.empty()); или же оно выстрелит на абсолютно валидном сценарии.
Вместо объекта передаем указатель на выделенную на куче память и где-то в методе_2 освобождаем ее за ненадобностью. Можно даже предположить, что мы имеем std::unique_ptr и перемещаем его. Хотя для такого случая я, скорее всего, задался бы вопросом, почему именно такой формат аргумента, и мог бы предположить, что по дороге он может быть освобожден.
Из хорошего: именно на этом примере у меня сформировалась аналогия логического use-after-free, описанная в тексте.
Из хорошего: именно на этом примере у меня сформировалась аналогия логического use-after-free, описанная в тексте.
Странно. А должно было бы: методы на тысячу строк -- это к беде. Вне зависимости от языка программирования.
А если там половина строк -- это комментарии? Тоже плохо?
А если там половина строк -- это комментарии?
500 строк комментариев в функции на 1000 строк.
К счатью, никогда такого ужаса не видел.
Ваш "логический" use after free ничем не отличается от того, что какой-то переменной присвоили новое значение о чем в какой-то момент забыли. Вроде вот такого:
void f() {
std::size_t i = 20;
g(i);
// Почему-то думаем, что в i значение не может быть меньше 20.
some_vect[i - 19] = 0xff;
}
...
void g(std::size_t & index) {
...
index = 0;
...
}
Отличается принципиально: в подобном коде всегда есть явное намерение автора для достижения некоторой поставленной задачи его алгоритма. Если это нетривиальный ход - желательно его откомментировать. Но я всегда могу сделать git blame и спросить у автора строки его понимание вещей напрямую.
Перемещение 9-го по счету параметра в вызываемый метод - это не более чем артефакт языка, который не несет никакой полезной нагрузки. Более того, его очень просто не заметить.
Отличается принципиально: в подобном коде всегда есть явное намерение автора для достижения некоторой поставленной задачи его алгоритма.
Пальцем можете показать в чем отличие?
Или вы хотите сказать, что из вашего метода_1 значение в метод_2 передавалось просто так, никакая поставленная задача не решалась?
Пальцем:
some_method(...., ...., ..., ...., ...., std::move(object), ..., ...., ..., );
и
some_method(...., ...., ..., ...., ...., object, ..., ...., ..., );
логически эквивалентны. Остальное - это детали реализации. Плохой реализации.
Жаль, что пришлось потратить на вас столько времени: если для вас эти фрагменты логически эквивалентны, то ваше мнение по поводу программирования можно смело отправлять в /dev/null.
Я понимаю, что std::move используют или не используют по ошибке, незнанию или недосмотру. Но утверждать что два таких вызова являются логически эквивалентными -- это уже за гранью добра и зла.
это еще хуже
Я тоже знаю, как должно быть правильно но я все еще не живу в идеальном мире. Разработка за деньги обычно предполагает сроки, взаимодействие с коллегами и это вот все. Увы.
Попахивает кашей с ownership-ом)
Как уже сказали выше, методы на 1000 строк и функции на 9 аргументов - это говнокод. Не надо так писать, и будет меньше проблем. Даже 100 строк для функции - уже много, если там не какой-то switch с минимумом логики. Функция должна быть такой, чтоб её можно было уместить в голове. Проблемы у вас возникли как раз
Само по себе
use-after-moveне является ошибкой, в отличии отuse-after-free. Объект после перемещения должен находиться в консистентном (но не обязательно определенном) состоянии. А объект в консистентном состоянии очень даже можно использовать (например, очистить и присвоить новое значение). Просто не стоит ожидать, что после перемещения объект будет таким же, как и после него.Если вы хотите быть уверенным, что аргумент, полученный функцией, не изменился где-то в середине функции, то просто сделайте его константным! Для этого и придумали константность. Но переместить его уже не получится, да.
Для меня C++ является языком, требующим колоссального внимания в силу неимоверного количества правил, исключений и просто абсолютно нелепых конструкций с точки зрения языкостроения.
C++ хорош тем, что можно не использовать все навороты языка одновременно. Какой-нибудь один гайд-стайл себе выбрать (а зачастую он уже наработан в проекте) и писать только на нем.
C++ хорош тем, что можно не использовать все навороты языка одновременно
Это соображение хорошо работает, когда ведёшь проект в одиночку, или в команде принята дисциплина и единомыслие. И когда большая часть кода - своя, и не состоит в основном из legacy, авторы которого оставили свой неповторимый стиль в коде прежде, чем окончательно исчезнуть с радаров.
В реальности чаще получается ограничивать себя, но не соседа/соседний отдел/авторов библиотеки с гитхаба, которую вам посчастливилось использовать в проекте.
В реальности чаще получается ограничивать себя
Оно бы звучало весомо... Только вот это говорит человек, который пишет на чистом Си и go, а не на C++ (и даже толком не знает C++, что уже неоднократно было продемонстрировано на RSDN).
В C++ гораздо чаще приходится ограничивать себя тем, что есть в компиляторах.
В C++ гораздо чаще приходится ограничивать себя тем, что есть в компиляторах.
Я примерно про это и говорю. Угу, тем, что есть в компиляторах...
Только вот это принципиально отличается от высказанного выше:
Какой-нибудь один гайд-стайл себе выбрать (а зачастую он уже наработан в проекте) и писать только на нем.
Здесь тов. @8street наверняка говорил о фичах языка, которые выбираются (или отвергаются) безоностительно возможностей компилятора. И у которых, по хорошему, должна быть мотивация из категории "потому что". Например, запрет на использование исключений потому что real-time или низкоуровневый код драйвера. Или запрет на использование RTTI потому что в итоговом исполняемом файле остается слишком много информации об исходном коде. Или запрет на использование unified initialization syntax потому что запись v{x, y} будет вести себя неожиданным образом, если v -- это std::vector или std::string.
Как правило, такие рекомендации формируют подмножество языка, работа с которым не должна вызывать проблем (как у команды разработки, так и при эксплуатации).
"Не путайте туризм с иммиграцией".
Возможно, если начнёте писать на Rust за деньги, и его тоже возненавидите ))
Сходу вот нашёл чью-то боль и вот и вот.
Претензии - медленная компиляция, сложный для понимания синтаксис (например "Pin<Pin<&dyn LocalTrait>>") и т.п.
Вообще со временем начинаешь понимать, что нет идеального языка.
Я на досуге имел возможность немного поучаствовать в коммерческом проекте на Rust с открытым кодом. Оказалось, что файлы сборки ровно такие же, как в маленьких проектах, зависимости - все те же, только их больше, а сам код сильно напоминает то, что писал я в своих домашних подельях.
В общем, вкатиться и понять это можно было за считанные дни. Конечно, много зависит от авторов проекта, но я для себя вынес одно слово, которым охарактеризовал этот опыт: "единообразие". Код на Rust с первого дня кажется знакомым, в нем на порядок проще ориентироваться в сравнении с C++. На плюсовых проектах всегда ощущение, что тебя окунули в чан с... с ледяной водой, допустим, и заставили в нем барахтаться.
Я с вами согласен, что рутина будет везде, усталость и раздражение будут накапливаться от любой работы, но даже первое впечатление от серьезного проекта на Rust разительно отличалось от такового для проектов на C++.
Отношение к Rust делит людей на два полярных лагеря: одни его любят за его явные преимущества, другие - беспричинно ненавидят.
Ох не люблю я эти темы... Вы говорите "К тому же я занимаюсь системным программированием, а это налагает дополнительные требования к знанию языка."
Проблема ровно в том, что у меня с вами очень разное понимание "системного программирования". В моем мире этим термином называют тот код, который НЕПОСРЕДСТВЕННО работает с аппаратурой (все эти DMA каналы, контроллеры прерываний, таймера, системные таблицы, регистры периферии и самого процессора). Этот код - фундамент. Соответственно, любой язык, который прячет от меня этот код для меня неприемлем. В частности C++. Для меня не очевидно как именно код на плюсах будет преобразован в ассемблер, и как итоговый код будет меняться в зависимости от ключей оптимизации или используемого компилятора. Лучший вариант - ассемблер. Самый разумный компромисс - С (без всяких плюсов). Все, что описываете вы работает на уровне выше. Для меня это частный случай прикладного ПО. И да, я не очень понимаю как делить его на уровни.
А еще, чисто для аналогии, есть вполне традиционный подход в современном загородном строительстве. Перезаклад на фундамент. Его делают заведомо сильно более мощным, чем требуется. Rust - эти аналог такого подхода, перенесенный в мир вычислительной техники. В подавляющем большинстве случаев это работает. Хоть и стоит значительно дороже. И в этом смысле нет проблем - пусть будет Rust. Но как в строительстве остается спрос на фундаменты без перезаклада, так и здесь разного уровня ассемблеры, скорее всего, никуда не денутся. Не всех устроит тот самый перезаклад. Более того, в строительстве уже активно отходят от такого подхода. Ибо у него уже находятся объективные изъяны. Например повышенная усадка или разломы из-за увеличившейся массы, или повышенные требования к качеству материалов. И это не считая главного - цены вопроса. Лично у меня нет сомнений в том, что рано или поздно и с Rust будет ровно то же. Это довольно молодой язык. Но молодость проходит. Наивно полагать, что Rust останется "вечно молодым". С доказал, что умеет взрослеть и красиво стареть. Впрочем, помирать он пока не собирается. Более того - пока молодые у него учатся. И это хорошо.
Проблема ровно в том, что у меня с вами очень разное понимание "системного программирования". В моем мире этим термином называют тот код, который НЕПОСРЕДСТВЕННО работает с аппаратурой
Я тоже работаю с аппаратурой, но только в последние несколько лет моя аппаратура - это принтеры и сканеры. Это такие довольно сложные железки, которые разговаривают с хостом в основном по сети или по USB.
Желание иметь 100% видимость и контроль на стыке железа и софта очень понятно. Непонятно, зачем для этого ассемблер. В целом, компилятору можно доверять, обычно всё же он нагенерирует то, что написано (в рамках свобод, гарантированных ему спецификацией языка). Мешают библиотечные автоматизмы, которые могут влиять на семантику действий.
В этом плане Go, например - вполне годный язык. Потому что от него можно добиться того, чтобы памятью он управлял, а в протокольные дела не лез, оставив видимость и контроль, сравнимые с ANSI C. Да, времянку он не всегда гарантирует, но мои устройства не настолько чувствительны к времянке.
Для принтеров и сканеров, драйвера которых по сути фильтры - это допустимо. Для драйвера, скажем, сетевой или аудио карты - уже сложнее. В этом случае работает "доверяй, но проверяй". И если то, что было написано на С я могу проверить, то с остальными языками сложнее. А вот, допустим, переключатель задач (именно переключатель, а не более обобщенный термин "планировщик") непосредственно на языках высокого уровня почти всегда не реализуем. И даже Rust здесь будет бессилен. Исключительно ассемблер.
Для принтеров и сканеров, драйвера которых по сути фильтры - это допустимо
В некотором идеальном мире драйвера принтеров (но не сканеров) - это фильтры, которые преобразуют условный PostScript в условный URF или PWG-Raster.
В реальном мире там достаточно возни вокруг затыков аппаратуры. Причём понятие аппаратуры включает в себя заглюки firmware, которое там сложное и хрупкое. И там всё, как у людей (у прочих железок). Например, пока принтер "прогревается", он может весьма своеобразно реагировать на стандартные запросы.
Я писал драйвера сетевых карт, в т.ч, и Wi-Fi, с достаточно сложной логикой на хосте. Мне эта тема вполне знакома.
Переключатель задач на С я тоже, кстати, писал :)
То, что я самостоятельно менял сцепление, пружины и амортизаторы на своем авто не делает меня автомехаником. И даже ежегодное перекидывание колес этому никак не помогает. Как и опыт ремонта головки блока цилиндров с заменой и притиркой клапанов. В лучшем случае я что-то понимаю. Но не больше.
Сколько драйверов надо написать, чтобы из автолюбителя превратиться в автомеханика? :)
Достаточно ни одного - это очень разные специальности с очень разными инструментами. Даже если мультиметр электрика используется ими обоими. Между этими специальностями, безусловно, можно мигрировать. Но мастерство можно получить только с удовольствием занимаясь любой из них всю жизнь.
Я всю жизнь с удовольствием занимаюсь системным программированием, в диапазоне от того, что делается с привлечением осциллографа и до весьма высокоуровневых вещей типа разборок с системой печати.
Желание слазить на уровень ассемблера пару раз присутствовало и ни разу в итоге не оправдывалось.
Я просто столяр-универсал, а не специалист по закручиванию шурупов определенного размера и с определенной формой шляпки в древесину определенного вида. Могу сделать полированный шкаф из морёного дуба, а могу - сосновый гроб.
Желание слазить на уровень ассемблера пару раз присутствовало и ни разу в итоге не оправдывалось.
Так выпьем же за то, чтоб наши желания совпадали с нашими возможностями (с)
Просто не попадалось задач, где по другому в принципе нельзя (или крайне нецелесообразно). Как, например, тут.
Что до универсализма... Хороший подход. Только вот времена Гаусса и Леонардо да Винчи были давно, а любая универсальная вещь одинаково неудобна во всех вариантах в сравнении с узко специализированной.
Впрочем, вопрос баланса... В разных жизненных ситуациях он разный. Временам швейцарский нож очень хорош, а временами он же становится игрушкой, мало на что годной.
Так выпьем же за то, чтоб наши желания совпадали с нашими возможностями (с)
И чтоб нам ничего за это не было!
Просто не попадалось задач, где по другому в принципе нельзя (или крайне нецелесообразно). Как, например, тут.
Попадались. Я делал BSP для самодельного чипа на основе ARM (самодельного не в том смысле, что его делали любители, а в том, что в этом и заключался бизнес конторы, в чиподелии).
Но это ж немного совсем ассемблера и ассемблер там простой. Притом, можно ведь по-разному писать. Я в этом BSP даже раскодирование номера IRQ из битовой маски сделал на Си. Но не наивным циклом, а явно расписанным двоичным поиском. И после того, как убедился, что gcc для подобного кода для этого процессора генерит, в общем-то, тот же ассемблер, что я руками бы написал.
Что до универсализма... Хороший подход. Только вот времена Гаусса и Леонардо да Винчи были давно, а любая универсальная вещь одинаково неудобна во всех вариантах в сравнении с узко специализированной.
Человек - исключение. Универсализм достигается не за счёт поверхностного знания по широкому кругу областей, а за счет предпочтения изучения принципов и идей, а не конкретных реализаций. Принципов и идей в ИТ гораздо меньше, чем конкретных реализаций.
Принципов и идей в ИТ гораздо меньше, чем конкретных реализаций.
Вполне конкретную задачу добраться из Москвы в Санкт-Петербург можно реализовать разными методами. В разных условиях разные решения окажутся лучшими. Важно лишь понимать какой из возможных вариантов в данном конкретном случае окажется лучшим, и стараться не связываться с теми вариантами, где лучшим будет допрыгать за три года на левой ноге.
Но опять же, все они сводятся к определенным классам поездов, автобусов, самолётов и велосипедов, а не к конкретным номерам поездов
Обычно системное ПО определяется тем, что оно напрямую взаимодействует с операционной системой и/или с устройствами. Также в его задачи входит самостоятельный учет и контроль ресурсов системы во время выполнения. То есть, в отличие от прикладного ПО, здесь меньше слоев абстракции между кодом и непосредственно железом.
Соответственно, любой язык, который прячет от меня этот код для меня неприемлем. В частности C++. Для меня не очевидно как именно код на плюсах будет преобразован в ассемблер, и как итоговый код будет меняться в зависимости от ключей оптимизации или используемого компилятора.
Вот как раз в плюсах достаточно много уровней абстракции прозрачно отображаются на ассемблер. Причем, программист волен сам выбирать, какие уровни ему нужны и понятны, а какие - уже избыточны. И дают они довольно много безопасности или удобства практически бесплатно.
Языков со свободой выбора, увы, не так уж и много.
Возможно. Может быть, за то время пока я "не брал в руки шашек", мир стал лучше. Но, тем не менее, даже понятные мне абстракции, кажутся избыточными для тех проектов, с которыми приходится иметь дело.
Возможно, мне просто везёт и эти самые абстракции используют те, кто на моем фундаменте архитектурный шедевр возводит. Обычно я на эту роль не претендую. А может "везёт тому, кто везёт". В том смысле, что если заниматься тем, что тебе понятно и приятно, особенно если на это ещё и есть спрос, то находятся другие ниши для дальнейшего саморазвития.
Но так или иначе, а плюсы меня коснулись боком и довольно давно. На сегодняшний день я вполне солидарен с автором. В моем "мире фундаментов" Rust имеет больше шансов, чем плюсы. Правда, есть некоторое ощущение, что сегодня он именно в этом мире, не более чем С с очень своеобразным синтаксисом и методикой сборки. Но посмотрим. Возможности предоставлены. Ждём результат.
У меня плохие новости, у Раста как раз тоже есть скрытые накладные расходы и встроенные неявные паники =)
Из-за чего, собственно, Торвальдс на него резко и наезжал.
Так что рекомендую уточнить детали.
Нулевыми абстракциями хвастается Zig, он и попроще и поудобнее для эмбеда.
Ну так паники вместо встроенных неявных сегфолтов в C. А если при неправильном использовании все равно падать, то какие-то странные наезды получаются.
Я в курсе. Но раз взяли, да еще и статус экспериментального сняли - значит путь открыт. Можно пользоваться и наблюдать за результатами.
Что до "падать" из сообщений ниже, то основная претензия к С, как я понимаю, ровно обратная. Не падает он, а продолжает работать с "отравленными" данными. И тут да - скользкий момент. Не всегда понятно какое поведение лучше. Сильно зависит от точки зрения и применения конкретно взятого изделия в конкретно взятых условиях. В целом, идея "падать по любому чиху" обычно мне не кажется правильной. Какой-нить зонд на уловном Марсе, по моим представлениям, должен переварить ошибку и продолжить работу. Мне кажется, вариант "упасть" там, пусть и с перезагрузкой, сильно менее желателен. Впрочем, повторюсь - сильно Application specific.
Основной посыл статьи от меня ускользает. Кажется, что ключевое -- это:
Диалога все равно, скорее всего, не выйдет.
Но если диалога между растаманам и плюсодрочерфилами не выйдет, то зачем нужно было эту статью публиковать?
Я могу вам предложить несколько посылов на выбор:
Любой осмысленный текст - это пища для ума. Возможно, конкретно эта пища не в вашем вкусе.
Текст призывает задуматься и понять, что в професии важно лично вам.
Иногда проблема действительно не в вас, а в окружении.
Диалог возможен, но только при взаимном интересе сторон к такому диалогу. Отсюда и оговорка о "скорее всего".
Иногда просто не стоит искать посыла, а насладиться формой, подачей и хорошо проведенным временем.
Я могу вам предложить несколько посылов на выбор
Не нужно. Лучше поделиться тем смыслом, который у вас был когда вы решили опубликовать здесь этот короткий очерк. Если этот самый смысл был. И если он не состоит в "какое афигительное открытие я совершил: на растаманов и плюсофилов можно повесить новые ярлыки!"
Иногда просто не стоит искать посыла, а насладиться формой, подачей и хорошо проведенным временем.
Проссыте, не нашел здесь ни формы, ни подачи. Могло бы выглядеть как банальность "кому-то нравится арбуз, а кому-то свинной хрящик, поэтому не нужно пытаться любителям арбузов объяснить прелесть свинного хрящика", но больше похоже на "а разведу-ка я еще один бессмысленный и беспощадный срач".
От срачей C++ vs Rust получаешь гораздо больше полезной информации когда идут сравнения "в C++ можно вот так, а в Rust-е вот так". Тут хоть есть о чем подумать и чему поучиться. Но эта статья явно не тот случай, отсюда и вопрос: так в чем же смысл?
Я же в самом первом пункте вам сказал: возможно, конкретно эта пища не в вашем вкусе.
И да: в нашем варианте диалога действительно не получится. Спасибо за внимание!
конкретно эта пища не в вашем вкусе
Мне только хотелось узнать, а в чем вы эту самую "пищу" видите. Но раз столь простые вопросы оскорбляют тонкие струны души автора, то увы и ах.
Я вам перечислил пункты, которые считаю посылами статьи. Если крупица о логическом use-after-free для вас не является новостью - поздравляю!
Лично я всегда нахожу даже мельчайшие нюансы чужих опусов интересными и оставляющими некое новое впечатление и простор для мыслей.
Но все люди разные, у кого-то струны контрабаса, у кого-то - скрипки. Кто-то резонирует от мельчайших колебаний, для кого-то и слон в комнате - не новость.
Если крупица о логическом use-after-free для вас не является новостью - поздравляю!
Не является. А если у вас не хватает ума понять, почему в C++ этот самый "логический" use-after-free существует, то вы напрасно причисляете себя к "белым воротничкам". Во многих C++ных нюансах есть своя логика, нужно только дать себе труд в этом разобраться (во многих, но не во всех, некоторые вещи реально приходится зазубривать).
Лично я всегда нахожу даже мельчайшие нюансы чужих опусов интересными и оставляющими некое новое впечатление и простор для мыслей
А я не страдаю политкоректностью и если на мой взгляд статья выглядит просто тупым и бесполезным набросом, то могу об этом прямо сказать. Особенно когда автор предпочитает разливаться мыслею по древу вместо прямых ответов на прямые вопросы.
Для меня C++ является языком, требующим колоссального внимания в силу неимоверного количества правил, исключений и просто абсолютно нелепых конструкций с точки зрения языкостроения.
приведите пару примеров нелепых конструкций
В комментарии выше было отличное видео.
Из моих любимых в наследии C:
звездочка отдельно от типа указателя при объявлении: int *p, *t;
присваивания в операторах
неявные преобразования типов
В плюсах:
порядок объявления членов vs порядок их указания в конструкторе
виртуальное наследование
конструкторы: A(const A&&) {} - разрешено, но зачем; A(A&) {} - пропущен const - и это уже не копирующий конструктор (увы, как-то видел такое в реальном коде, пришлось долго искать причину поломки). сам принцип того, что при объявлении конструктора все может взорваться где-то в другом месте, потому что конструктор по умолчанию больше не генерируется
lvalue, rvalue, prvalue, xvalue (ничего не пропустил?)
тут можно долго продолжать, но зачем?
Вспомнил ещё из любимого, когда смотрел в код: дублирование конструкторов копирования/перемещения и операторов =. Логика в них зачастую одинаковая, поэтому придумали несколько трюков, чтоб избегать дублирования. Но проблема-то простая: оператор = перегружен смыслом. Помимо копирования/перемещения, он он был спроектирован для поддержки редкого синтаксиса цепочечного присваивания a = b = c. В итоге имеем дополнительную проблему на ровном месте.
Логика в них зачастую одинаковая, поэтому придумали несколько трюков, чтоб избегать дублирования.
Каких именно? copy-then-swap или что-то другое?
Но проблема-то простая: оператор = перегружен смыслом
В чем именно проблема?
В итоге имеем дополнительную проблему на ровном месте.
Какую именно?
A(const A& a) { *this = a; }
И вся логика в операторе присваивания.
Проблема в том, что имеем по два члена-метода (а всего 4), делающих почти всегда одно и то же. Я встречал плохой код, где забывали что-то из этого или принципиально не добавляли, потому что логика ломалась.
Мне непонятно, зачем нужно два метода на одно и то же. Достаточно только конструктора, если перестать поддерживать цепочечное присваивание. На каждое обычное присваивание достаточно вызавать конструктор. Проблем с этим я не вижу.
A(const A& a) { *this = a; }
Это говнокод. За такое по рукам нужно бить.
Достаточно только конструктора, если перестать поддерживать цепочечное присваивание.
Почему вы думаете, что наличие отдельного operator= как-то связано с цепочечным присваиванием?
На каждое обычное присваивание достаточно вызавать конструктор.
И тут в дело вступает exception safety. Как минимум.
Почему вы думаете, что наличие отдельного operator= как-то связано с цепочечным присваиванием?
Если это не так - просветите, в чем необходимость двух сильно похожих методов?
Это говнокод. За такое по рукам нужно бить.
И в чем его реальная проблема, помимо вкусовщины?
И тут в дело вступает exception safety. Как минимум
То есть в операторе = какая-то особая exception safety? В чем принципиальная разница? В стандарте, кажется, исключение на неполностью сконструированном объекте вполне себе определено. Из этого следует, что если бы такой конструктор вызывался в месте присваивания, то объект был уже сконструирован ранее, поэтому правило выше не применялось. Но это надо было делать так сразу.
Если это не так - просветите, в чем необходимость двух сильно похожих методов?
Вы выдвинули тезис о том, что поддержка цепочечного присваивания здесь как-то причастна. Вам этот тезис и подтверждать.
И в чем его реальная проблема, помимо вкусовщины?
В двойной инициализации содержимого объекта. Например, вот в таком случае:
class demo {
some_class _a;
some_class _b;
some_class _c;
...
public:
demo(const demo & b) { *this = b; }
...
};
У вас для demo::_a, demo::_b, demo::_c сперва будет вызван дефолтный конструктор, затем еще и оператор копирования.
Кроме того, к моменту вызова operator= внутри конструктора копирования у вас могут быть нарушены инварианты класса (т.к. вы не присвоили начальное значение полям объекта). Что может затем вылезти боком, когда логика в operator= станет посложнее.
То есть в операторе = какая-то особая exception safety?
Как раз для конструктора объекта нет понятия exception safety. Если при конструировании объекта вылетело исключение, то объекта не будет. А раз нет объекта, то не для чего и exception safety обеспечивать.
А вот для operator= может потребоваться обеспечение strong exception safety. Хотя не факт, что вы в курсе шо це таке.
Из этого следует, что если бы такой конструктор вызывался в месте присваивания, то объект был уже сконструирован ранее, поэтому правило выше не применялось
Какой-то бессмысленный набор слов.
Вы выдвинули тезис о том, что поддержка цепочечного присваивания здесь как-то причастна. Вам этот тезис и подтверждать.
Конструкторы не возвращают значений, а оператор = возвращает ссылку на себя. Насколько я знаю, это нужно только для цепочечного присваивания.
У вас для demo::_a, demo::_b, demo::_c сперва будет вызван дефолтный конструктор, затем еще и оператор копирования.
Ок, для каких-то случаев это может быть существенно.
А вот для operator= может потребоваться обеспечение strong exception safety. Хотя не факт, что вы в курсе шо це таке.
Давайте тут закончим, вы меня просветили достаточно.
а оператор = возвращает ссылку на себя. Насколько я знаю, это нужно только для цепочечного присваивания
Из этого не следует, что сам по себе operator= нужен только для того, чтобы поддержать цепочечное присваивание.
Почему бы вам не подумать о том, что у конструктора и оператора присваивания логика может отличаться. Например, представим, что есть интерфейс observable и есть сигнлетон observer. Некий тип demo реализует интерфейс observable и регистрирует себя при создании:
class demo : public observable {
public:
demo() {
observer::instance().add(this);
}
demo(const demo & other) : ... {
observer::instance().add(this);
}
~demo() override {
observer::instance().remove(this);
}
...
};
Фокус в том, что когда экземпляр demo получает новое значение он не должен вычеркивать себе из observer-а. Ведь про этот экземпляр observer уже знает. Меняется только часть состояния экземпляра demo. Поэтому в operator=, в отличии от конструктора копирования, не будет обращений к observer-у.
Давайте тут закончим
Уверены? Есть ощущение, что вы о C++ еще очень многого не знаете, не смотря за декларируемые 20 лет опыта.
А то самое непонимание, к которому вы апеллируете как в статье, так и в комментариях, проистекает от незнания как раз.
Уверен.
Удачи в многопоточной реализации вашего подхода! Будете иметь иногда два сообщения, а также дергать мьютекс без надобности на каждом копировании. Контекст важен.
По поводу моего "говнокода": его едининственный недостаток - он не скомпилируется при отсутствии дефолтного конструктора у класса или одного из его членов. Но меня это не волнует. Остальную неэффективность выпилит оптимизатор. На случай возражений о тяжёлых дефолтных конструкторах - так это вот говнокод, не делайте так.
А ещё потрудитесь понимать, что вам пишут. Я все время замечаю, что вы от части комментируете что-то вам непонятное, но это проблема может быть с обеих сторон.
Вы прекрасно владеете инструментом, но явно не понимаете, что инструмент - это не все. И это как раз был один из посылов статьи. Хотите быть синим воротничком - ваше право.
Удачи в многопоточной реализации вашего подхода
Очень похоже, что когда вам нечем аргументировать, вы решаете достать очередной рояль из кустов. Многопоточность здесь вообще не при чем от слова совсем.
Конструктор -- это создание нового объекта с нуля. Не было, не было, и вдруг появился.
Оператор копирования -- это изменение существующего объекта.
Да, общего в них много, за счет переинициализации. Но из-за того, что логика операций может отличаться, приходится иметь оба этих понятия.
А что бы без проблем избежать дублирования уж лучше использовать идиому copy-then-swap, т.е. выражать копирование через конструирование, а не наоборот.
По поводу моего "говнокода": его едининственный недостаток
Чуть выше вы же:
Ок, для каких-то случаев это может быть существенно.
Шиза косит ваши ряды? В дополнение к незнанию и непониманию.
А ещё потрудитесь понимать, что вам пишут.
Так вопросы к вам возникают как раз из-за непонимания. Если бы я понимал, что вы пишете, то вопросов бы не было (или они были бы совсем другими), но я не понимаю, поэтому вопросы есть.
И это как раз был один из посылов статьи.
Я вам задавал вопросы о смысле вашей статьи, вы не смогли на них внятно ответить.
Хотите быть синим воротничком - ваше право.
Ну вы бы хоть происхождение терминов "синий" и "белый" применительно к воротничкам погуглили. А то зная историю их происхождения ваше деление на "синих" и "белых" вообще выглядит бессмыслено. Может вы думаете, что в начале XX-го века перекладывание заплатных ведомостей в офисе "белым воротничком" было более интеллектуальным занятием чем настройка конвейера на заводах Форда "синими воротничками"?
Кстати, ещё два вопроса к вашему примеру: вы правда считаете синглтон и кардинально различное поведение конструктора копирования и оператора = хорошим стилем?
вы правда считаете синглтон
Синглетон -- это всего лишь паттерн проектирования. Когда-то уместный, когда-то нет. Чаще нет. Но это не значит, что он бесполезен. Для логирования, например, синглетоны вполне себе нормальная штука.
различное поведение конструктора копирования и оператора
Во-первых, логика подсказывает, что конструктор и оператор копирования могут иметь различающуюся реализацию. А раз так, значит надо дать программисту возможность эту разную реализацию записать, если это ему потребуется. C++ изначально заточен на гибкость и на адаптацию под нужды конкретной задачи (не всегда хорошо, но это уже другой вопрос).
Во-вторых, если брать в рассмотрение вопрос exception safety, то как только у нас возникает потребность в strong exception safety, то мы просто вынуждены специальным образом реализовывать оператор копирования. А для этого опять таки нужно, чтобы конструктор копирования был отдельно, а оператор копирования -- отдельно.
Так что вопрос не в том, хороший ли стиль или нет. Вопрос в том, сможет ли язык позволить программисту извратиться так, как программисту (а не языку программирования) нужно.
А теперь позвольте мне вам разложить все по пунктам.
Если вы применяете синглтон, то обычно это многопоточный код, потому что я не очень понимаю, как вы ограничите создание вашего класса в пределах одного потока.
Я могу предположить, что внутри себя синглтон использует thread_local, но это как минимум странно, а также сильно усложняет понимание кода и его тестирование.
Для однопоточного случая обычно проще передать ссылку в конструкторе - это куда читабельнее.
Далее, обозреватель - в принципе плохой паттерн. Он разрывает локальность графа выполнения и затрудняет понимание. Я вдвойне не понимаю, зачем он нужен в однопоточном коде.
Вернемся теперь к этому:
some_method(...., ...., ..., ...., ...., std::move(object), ..., ...., ..., );
и
some_method(...., ...., ..., ...., ...., object, ..., ...., ..., );
Вы так кичитесь своим знанием языка, что забыли спросить, какого типа здесь object и какова семантика перемещения его класса. Вот так одна строчка кода оставила в дураках большого знатока с большим апломбом. А ведь такое может попасться в реальном коде, и у меня, в отличие от вас, выработалась паранойя досконально проверять все плохо пахнущее.
Потом: я специально придумал случай с 1000 строками кода, потому что я именно ждал от вас и таких, как вы, аппеляций к личности, каких-то там ярлыков кривокодера и прочего. Вы даже не в состоянии понять, что в любом количестве строк кода, которые вы считаете приемлемым, эта ошибка никуда не денется.
Более того, как я уже аргументировал, эта ошибка - артефакт языка, ваши и подобные примеры очень легко перенести на любой другой язык, и они там будут столь же некорректны. А мой - применим только к C++.
Ок, для каких-то случаев это может быть существенно.
Здесь вы даже не в состоянии понять, что моя реплика равносильна: "существуют случаи, для которых это существенно" в логике первого порядка.
По поводу моего "говнокода": его едининственный недостаток - он не скомпилируется при отсутствии дефолтного конструктора у класса или одного из его членов. Но меня это не волнует.
Далее я привожу вам тот самый пример, когда это действительно существенно, и делаю оговорку, что меня это не волнует.
При этом вы очень прыткий и хорошо владеете языком, но не понимаете, как его применять. На таких, как вы, я насмотрелся на собеседованиях: обычно это вчерашние студенты, вызубрившие "банду четырех" и "свой любимый C++", и выливающие все эти знания на специалиста в надежде показаться крутыми.
Именно поэтому вы не в состоянии читать чужие мысли в тексте - вы просто к этому еще не готовы. Но при этом вы снова-таки легко перекладываете свое непонимание на других, еще и в достаточно грубой форме.
Я надеюсь, вы прочтете это и сначала задумаетесь, прежде чем выпаливать очередной несуразный комментарий.
Всего доброго! Живите счастливо!
А теперь позвольте мне вам разложить все по пунктам.
Да сколько угодно. Главное, чтобы хоть что-то у вас получилось. Но, боюсь, не в этот раз.
Если вы применяете синглтон, то обычно это многопоточный код
еще и в огороде бузина, а в Киеве дядька. Сиглетон к многопоточности имеет даже не ортогональное отношение.
Я могу предположить, что внутри себя синглтон использует thread_local
Вы можете предполагать все что угодно. Но хотелось бы, чтобы эти предположения: а) были обоснованными и b) имели непосредственно отношение к предмету разговора. Пока нет ни того, ни другого.
Для однопоточного случая обычно проще передать ссылку в конструкторе - это куда читабельнее.
Передача что ссылки, что указателя, к вопросу многопоточности отношения не имеет. Как и взятая вами хз откуда многопоточность не имеет отношения к разделению конструктора и операторов копирования.
Далее, обозреватель - в принципе плохой паттерн.
Продолжаете держать нас в курсе, ваше мнение очень важно.
Я вдвойне не понимаю, зачем он нужен в однопоточном коде.
Я втройне не понимаю откуда из примера про различия в реализация конструктора и оператора копирования вдруг пошли рассуждения о многопоточности и плохости паттерна "обозреватель".
Вы так кичитесь своим знанием языка, что забыли спросить, какого типа здесь object и какова семантика перемещения его класса
Как бы не кичусь. И как бы спрашивать здесь незачем и не о чем. Речь о том, что если для вас две эти строки логически эквивалентны, то ваше мнение о программировании вообще (не говоря уже о программировании на C++) множится на ноль.
я специально придумал случай с 1000 строками кода
Т.е. вы предложили обсуждать не реальный кейс из реальной жизни, а какую-то свою фантазию? Я сейчас правильно понял?
ваши и подобные примеры очень легко перенести на любой другой язык
Попробуйте перенести примеры с конструктором и оператором копирования на любой другой язык.
Здесь вы даже не в состоянии понять, что моя реплика равносильна: "существуют случаи, для которых это существенно"
Я это прекрасно понял, но тут важно, что вы признаете существование указанной мной проблемы.
Далее я привожу вам тот самый пример, когда это действительно существенно
А этот пример нет смысла рассматривать, т.к. в этом случае ваш говнокод даже не скомпилируется. Т.е. человек не столкнется с тем, что говнокод компилируется, работает, но работает как-то не то чтобы очень. По крайней мере с лишними накладными расходами.
и делаю оговорку, что меня это не волнует.
Если бы C++ делался персонально под вас, эта оговорка могла бы иметь смысл. Но язык используется миллионами разработчиков и какой-то процент от этих миллионов не захочет тратить лишние такты на никому не нужную работу.
При этом вы очень прыткий и хорошо владеете языком, но не понимаете, как его применять
А вот здесь бы пруфов. Откуда вам знать про мое применение C++?
Извините но у меня созрел вопрос, вы случаем не наркоман ли?
Если вы применяете синглтон, то обычно это многопоточный код
wat?! Как у вас в мозгу вообще образовалась такая ассоциация?
Я могу предположить, что внутри себя синглтон использует thread_local
wat?! wat?! Ещё одно чудное предположение, которое мало того что взялось из воздуха так ещё и во многом противоречит идеи синглтона (синглтон - одиночка, значит 1, один, one экземпляр, а thread_local создаёт по экземпляру на каждый поток).
А по поводу вашего гениального решения избавится от оператора присваивания и оставить только конструктор копирования, подскажите как тогда реализовать хотя бы указатели с подсчётом? Не говоря уже о чём-то более сложном.
Извините но ваша экспертность в знании С++ совершенно не бьётся с тем что вы пишите.
Отношение к Rust делит людей на два полярных лагеря: одни его любят за его явные преимущества, другие - беспричинно ненавидят
Довольно спорное утверждение про ненависть. Недолюбливают представителей сообщества rust, которые иногда устраивают холивар на ровном месте, это да. А язык сам по себе имеет плюсы и минусы, как любой другой.
А теперь посмотрите на это под таким углом: где у нас в жизни одним специалистам нужны память и внимание, а другим - логика? Так ведь ясно, что к первой группе относятся мастера, операторы станков и инструментов и прочие синие воротнички. А что со второй группой? Это инженеры, математики и люди подобных профессий - белые воротнички.
Здесь зачем-то присутствует манипуляция, связанная с ассоциациями. Самое смешное я знаю очень много инженеров, которые пишут довольно наукоемкий софт для дронов, различных встраиваемых систем и систем моделирования физических систем на С++ и С.
PS Мне раст нравится, но в силу многих объективных причин я пишу на С++. Для RnD задач он бывает удобен.
Для многих инженеров выбор C или C++ десятилетиями был предопределен родом деятельности. Я не от большой любви на них пишу, но очень долго альтернатив-то не было. Думаю, со временем очень многие из них придут к Rust. Я уже видел, что в некоторых научных статьях начали давать примеры кода на Rust.
Вообще, плюсы не учат хорошему вкусу в программировании. Возможно, кстати, для людей с наукоемким софтом это и не так важно: у них задача другая. Но вот когда кода реально много - тут ведь без вариантов.
Отношение к Rust делит людей на два полярных лагеря: одни его любят за его явные преимущества, другие - беспричинно ненавидят.
Я бы предложил другую классификацию: одни терпеть его не могут из-за секты свидетелей Rust, другие - беспричинно обожают.
Многим людям, таким как я, наконец-то дали инструмент, на котором можно творить в потоке: лить код из головы и не отвлекаться на шероховатости языка, перепроверяя себя каждую секунду. С Rust я впервые почувствовал, что такое быть "высокоуровневым" программистом. Это очень крутое и вдохновляющее ощущение. Думаю, за фанатизмом любителей Rust скрывается благодарность за подобную отдушину.
логический use-after-free, возникающий в результате доступа к объекту, из которого выполнено перемещение
Понятнее такое называть use-after-move - чего нет в Rust, в отличие от C++, как вы верно указали
C++, Rust и цветовая дифференциация воротничков