Pull to refresh

Comments 82

"...логический use-after-free..." - простое правило, не обращайся к объекту в moved-from стейте (даже если очень хочется, и есть сто кейсов когда нужно и т.д и т.п.). Можно не использовать move вообще. Вообще можно не использовать вещи которые не нужны, а маргинального говна в c++ полно, хоть тоже виртуальное наследование.

"Я не умею запоминать, я умею понимать" - это норм, причём в отношении любого языка. Стоит понимать что к формированию C++ и его библиотек и компиляторов причясны тысячи человек на протяжении десятков лет, естествено что один человек не способен осознать весь этот корпус знаний.

То что автор назвал логическим use after free это буквально относится к любому обращению к переменной после её изменения. Например после swap или просто вы сделали clear на векторе и потом обратились к нему.

В общем претензия самая нелепая из всех что представители языка ада2 предъявляли С++

Я в параллельной ветке привел пример, из которого прямо следует, что графы исполнения для физического и логического use-after-free изоморфны, а, значит, если вы признаете существование физического use-after-free, то следеут уже признать и существование его логического эквивалента в C++.

undefined behavior это понятие из стандарта С++, если в том же стандарте нет понятия logical undefined behavior, значит его нет

Да, вместо этого там есть что-то типа: "the object is left in a valid, but unspecified state". Хорошая попытка.

Я понял правильную аргументацию: возьмите ваш пример с некорректной очисткой вектора и перенесите его в любой другой язык программирования. Ошибка кодирования сохранится, алгоритм будет работать некорректно. А теперь попробуйте сделать то же самое с предложенным мной ниже примером с перемещением.

В этом и есть разница: семантика перемещения - это артефакт отдельного языка, служащий "костылем" для повышения производительности, который сам по себе был рождён в результате неверно принятых концептуальных решений предыдущих этапов.

У меня однажды было так:

метод_1 на тысячу строк перемещает один из своих аргументов в метод_2 где-то в середине своего тела. Мне понадобился доступ к этому аргументу сильно ниже в методе_1. Хорошо, что я имею годами выработанную привычку обмазывать все неизвестные мне вещи ассертами. Я поймал ошибку на CI.

Я даже знаю, как мы к этому пришли: когда-то метод_1 был небольшим, а в методе_2 нужно было изменять переданный аргумент, поэтому его принимали по значению. Логичным решением было переместить имеющийся в методе_1 аргумент. Потом метод_1 отрос подробностями, но код продолжал работать. Пока не пришел тот, кому нужно было этот аргумент зачем-то использовать.

А теперь я вам расскажу два альтернативных сценария:

  1. Вместо одиночного объекта в метод_1 приходит коллекция. Искушение переместить ее значительно возрастает. Но с коллекцией проблема серьезней: очень часто пустая коллекция - это вполне норма. Поди разберись, нужно ли вставлять в коде где ни попадя assert(!vec.empty()); или же оно выстрелит на абсолютно валидном сценарии.

  2. Вместо объекта передаем указатель на выделенную на куче память и где-то в методе_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 используют или не используют по ошибке, незнанию или недосмотру. Но утверждать что два таких вызова являются логически эквивалентными -- это уже за гранью добра и зла.

То есть сложный алгоритм на полтысячи строк не должен быть прокомментирован? Не завидую вашим читателям

Я тоже знаю, как должно быть правильно но я все еще не живу в идеальном мире. Разработка за деньги обычно предполагает сроки, взаимодействие с коллегами и это вот все. Увы.

Да да, классические отмазки кривокодеров, времени нет, бизнес бла бла бла. Метод на тысячу строк это говнокод и нет этому оправдания, методы на 9 аргументов также плохо пахнут.

Попахивает кашей с ownership-ом)

  1. Как уже сказали выше, методы на 1000 строк и функции на 9 аргументов - это говнокод. Не надо так писать, и будет меньше проблем. Даже 100 строк для функции - уже много, если там не какой-то switch с минимумом логики. Функция должна быть такой, чтоб её можно было уместить в голове. Проблемы у вас возникли как раз

  2. Само по себе use-after-move не является ошибкой, в отличии от use-after-free. Объект после перемещения должен находиться в консистентном (но не обязательно определенном) состоянии. А объект в консистентном состоянии очень даже можно использовать (например, очистить и присвоить новое значение). Просто не стоит ожидать, что после перемещения объект будет таким же, как и после него.

  3. Если вы хотите быть уверенным, что аргумент, полученный функцией, не изменился где-то в середине функции, то просто сделайте его константным! Для этого и придумали константность. Но переместить его уже не получится, да.

Для меня 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 для подобного кода для этого процессора генерит, в общем-то, тот же ассемблер, что я руками бы написал.

Что до универсализма... Хороший подход. Только вот времена Гаусса и Леонардо да Винчи были давно, а любая универсальная вещь одинаково неудобна во всех вариантах в сравнении с узко специализированной.

Человек - исключение. Универсализм достигается не за счёт поверхностного знания по широкому кругу областей, а за счет предпочтения изучения принципов и идей, а не конкретных реализаций. Принципов и идей в ИТ гораздо меньше, чем конкретных реализаций.

Принципов и идей в ИТ гораздо меньше, чем конкретных реализаций.

Вполне конкретную задачу добраться из Москвы в Санкт-Петербург можно реализовать разными методами. В разных условиях разные решения окажутся лучшими. Важно лишь понимать какой из возможных вариантов в данном конкретном случае окажется лучшим, и стараться не связываться с теми вариантами, где лучшим будет допрыгать за три года на левой ноге.

Но опять же, все они сводятся к определенным классам поездов, автобусов, самолётов и велосипедов, а не к конкретным номерам поездов

Так я ж не против. Появился новый беспилотный Камаз (Rust) и в определенных условиях его использование становится оптимальным. Но в других условиях Сапсан он не заменит.

Обычно системное ПО определяется тем, что оно напрямую взаимодействует с операционной системой и/или с устройствами. Также в его задачи входит самостоятельный учет и контроль ресурсов системы во время выполнения. То есть, в отличие от прикладного ПО, здесь меньше слоев абстракции между кодом и непосредственно железом.

Соответственно, любой язык, который прячет от меня этот код для меня неприемлем. В частности C++. Для меня не очевидно как именно код на плюсах будет преобразован в ассемблер, и как итоговый код будет меняться в зависимости от ключей оптимизации или используемого компилятора.

Вот как раз в плюсах достаточно много уровней абстракции прозрачно отображаются на ассемблер. Причем, программист волен сам выбирать, какие уровни ему нужны и понятны, а какие - уже избыточны. И дают они довольно много безопасности или удобства практически бесплатно.

Языков со свободой выбора, увы, не так уж и много.

Возможно. Может быть, за то время пока я "не брал в руки шашек", мир стал лучше. Но, тем не менее, даже понятные мне абстракции, кажутся избыточными для тех проектов, с которыми приходится иметь дело.

Возможно, мне просто везёт и эти самые абстракции используют те, кто на моем фундаменте архитектурный шедевр возводит. Обычно я на эту роль не претендую. А может "везёт тому, кто везёт". В том смысле, что если заниматься тем, что тебе понятно и приятно, особенно если на это ещё и есть спрос, то находятся другие ниши для дальнейшего саморазвития.

Но так или иначе, а плюсы меня коснулись боком и довольно давно. На сегодняшний день я вполне солидарен с автором. В моем "мире фундаментов" Rust имеет больше шансов, чем плюсы. Правда, есть некоторое ощущение, что сегодня он именно в этом мире, не более чем С с очень своеобразным синтаксисом и методикой сборки. Но посмотрим. Возможности предоставлены. Ждём результат.

У меня плохие новости, у Раста как раз тоже есть скрытые накладные расходы и встроенные неявные паники =)

Из-за чего, собственно, Торвальдс на него резко и наезжал.

Так что рекомендую уточнить детали.

Нулевыми абстракциями хвастается Zig, он и попроще и поудобнее для эмбеда.

Ну так паники вместо встроенных неявных сегфолтов в C. А если при неправильном использовании все равно падать, то какие-то странные наезды получаются.

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

Без сюрпризов.

А ещё бывают трап-ловушки для обработки, не говоря уж об SEH.

Втихую упасть оно любой дурак сможет

Я в курсе. Но раз взяли, да еще и статус экспериментального сняли - значит путь открыт. Можно пользоваться и наблюдать за результатами.

Что до "падать" из сообщений ниже, то основная претензия к С, как я понимаю, ровно обратная. Не падает он, а продолжает работать с "отравленными" данными. И тут да - скользкий момент. Не всегда понятно какое поведение лучше. Сильно зависит от точки зрения и применения конкретно взятого изделия в конкретно взятых условиях. В целом, идея "падать по любому чиху" обычно мне не кажется правильной. Какой-нить зонд на уловном Марсе, по моим представлениям, должен переварить ошибку и продолжить работу. Мне кажется, вариант "упасть" там, пусть и с перезагрузкой, сильно менее желателен. Впрочем, повторюсь - сильно Application specific.

Как раз для зонда продолжает работать с "отравленными" данными может привести к печальным последствиям. Например он перестанет отвечать на внешние команды и будет потерян.

В коде для зондов стараются обработать все возможные ошибки. Но если пропустили, то лучше бы упасть чем UB.

Основной посыл статьи от меня ускользает. Кажется, что ключевое -- это:

Диалога все равно, скорее всего, не выйдет.

Но если диалога между растаманам и плюсодрочерфилами не выйдет, то зачем нужно было эту статью публиковать?

Я могу вам предложить несколько посылов на выбор:

  1. Любой осмысленный текст - это пища для ума. Возможно, конкретно эта пища не в вашем вкусе.

  2. Текст призывает задуматься и понять, что в професии важно лично вам.

  3. Иногда проблема действительно не в вас, а в окружении.

  4. Диалог возможен, но только при взаимном интересе сторон к такому диалогу. Отсюда и оговорка о "скорее всего".

  5. Иногда просто не стоит искать посыла, а насладиться формой, подачей и хорошо проведенным временем.

Я могу вам предложить несколько посылов на выбор

Не нужно. Лучше поделиться тем смыслом, который у вас был когда вы решили опубликовать здесь этот короткий очерк. Если этот самый смысл был. И если он не состоит в "какое афигительное открытие я совершил: на растаманов и плюсофилов можно повесить новые ярлыки!"

Иногда просто не стоит искать посыла, а насладиться формой, подачей и хорошо проведенным временем.

Проссыте, не нашел здесь ни формы, ни подачи. Могло бы выглядеть как банальность "кому-то нравится арбуз, а кому-то свинной хрящик, поэтому не нужно пытаться любителям арбузов объяснить прелесть свинного хрящика", но больше похоже на "а разведу-ка я еще один бессмысленный и беспощадный срач".

От срачей 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++, как вы верно указали

Спасибо, запомню.

Sign up to leave a comment.

Articles