Pull to refresh
4
0
Send message

(я не автор поста) По-моему, смысл & - создать временный объект ValuePointer, с вызовом guard в конструкторе и unguard в деструкторе. Ну а * - собственно взять завёрнутое в этот временный ValuePointer значение, но! - с дополнительными приспоблениями, реализованными в guard / unguard (ну, потоковая безопасность, что-то такое). Польза!

Конечно, невнимательный прикладной программист может случайно забыть добавить *& -- и не получить пользы. Код будет как бы работать, но не совсем: что-то крэшнется, что-то полетит не туда. Ну а кто сказал, что профессия программиста - простая и лёгкая? Программист должен быть внимательным!

В pdf-е этого переиздания не хватает обложки с портретом Киселёва - её можно увидеь здесь (издание - не коммерческое, стоимость бумажной копии равна затратам на производство этой копии на одном из print-on-demand сервисов):
https://www.amazon.com/Геометрия-Киселёву-Russian-Петрович-Киселёв/dp/1684748127/

Для полноты (на случай, если кому-либо окажется полезным) - вот современное переиздание учебника Киселёва: https://arxiv.org/abs/1806.06942

Переводчик вам вряд ли ответит на вопрос к автору - они работают в разных фирмах и вряд ли общаются напрямую. В заметке есть ссылка на следующий пост автора, посвящённый именно тестированию:
https://betterprogramming.pub/quit-unit-testing-classes-and-use-a-behavior-oriented-approach-306a667f9a31
Кратко - на юнит тестирование забили большой болт, но, вроде как, выстроили супер-дупер систему интеграционного тестирования. По мне - есть риск, что когда аторы супер-дупер системы интеграционного тестирования переместятся на более зелёные пастбища, она заржевеет, и следующий набор сотрудников будет или её переписывать, или таки-восстанавливать возможность юнит-тестирования. Моё персональное мнение, ни в коей мере не претендующее на близость к реальности, так красиво (с картинками!) изображённой автором.

Отличная практика в проекте, в котором вы владеете coding guidelines и можете в одночасье расставить везде const, да ещё и поддержать практику статической проверкой (такие инструменты есть). В Rust то, что неизменяемость идёт по дефолту (ничего не надо писать, наоборот, надо писать mut для изменяемости) - замечательно. А в C++ дефолт, увы, противоположный. Согласен с спецификацией const для не-мутирующих функций-членов, но вот, const для каждой локальной переменной (и параметров, переданных по значению) - редко встречаю такое, так что ваш тезис мне понятен, но сам на практике предпочёл в данный период не применять (пока - из-за отсутствия технической возможности поддержать статическим анализом). Когда под рукой будет на готове опция статической проверки (что все немутирующие переменные и методы помечены const) - обсужу с коллегами и, возможно, начнём внедрять. В соло (хобби-)проектах - тоже за! Но, в коллективном проекте с большим объёмом кода без соблюдения этого правила и без возможности поддержки правила статическим анализом - взвесив за и против, решил пока так. Первый приоритет для меня - получить возможность обеспечить такую проверку статическим анализом (до этого - нет смысла и обсуждать с коллегами!), потом - достичь консенсуса с коллегами, что они согласны - эта новая строгость пойдёт на пользу в перспективе, а потом уже - внедрять. Внедрять на существующем коде, где такого правила нет, без поддержки автоматической проверкой - гиблый номер.

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

Что вы говорите, пишете коллеге, реализовавшем цикл, в котором непонятно, почему произойдёт завершение, и будет ли при завершении гарантирован какой-либо результат?

Используете ли вы в таком случае в диалоге термины вроде "инвариант цикла", "условие завершения"? Не предлагаете ли вы такому коллеге от греха подальше пользоваться более выскоуровневыми конструкциями типа range loop (специально проверил, что в Go есть такой)?

Если да, то - и для вашей практической пользы "учёные развлекались не зря" (какое-то время назад эти понятия не существовали, и какое-то время заняло признание их полезности большинством активных программистов, имеющих дело с циклами).

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

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

Обратный пример, отрицательный, но он вам должен быть близок. Perl - замечательный язык программирования, с высокоуровневыми конструкциями и компактной записью программ. Практичный (мой любимый пример - в "научной" терминологии ассоциативный массив, словарь или map - честно назван в ядре языка hash, ну чтобы сразу практичному программисту было понятно, что там внутри). У него были замечательные перспективы, активная аудитория практикующих программистов. На нём было легко создавать работающие программы! Более того, этот язык был создан начинающим программистом - и исходя из его практических потребностей (замечу, эта история создания языка программирования отличается от истории создания Go). Когда и что с Perl пошло не так, как вы считаете? Я недавно узнал, язык переименовали... зачем - может быть, чтобы сбросить балласт каких-то отрицательных коннотаций? Но что конкретно?! У вас есть мнение на этот счёт? Мне, честно говоря, Perl не нравился, но я никогда на нём не программировал, так что моё мнение - неквалифицированное. Однако, могу вздохнуть свободно - ныне это название языка могу смело выкидывать из головы (а переименованное даже не трудиться запоминать).

Мне стало любопытно - первым делом нашлась документация о том, как это делается с Coq:

https://www.cis.upenn.edu/~plclub/blog/2020-10-09-hs-to-coq/

Хорошо, давайте обсудим, и я извиняюсь за эмоциональность. Давайте оставим в стороне кейс с glibc. Я поясню, почему интересуюсь именно TLA+ (по названию инструмента нашёл кейс).

Один повод - тот факт, что высоконагруженная система не всегда даёт лучшие показатели с высокоуровневыми примитивами параллелизма. Приходится использовать lock-free структуры данных (не знаю, как с ними в Go, на C в стандартную библиотеку не входят), и я не уверен, что мне не придётся ловить / отлаживать баг, подобный описанному в том блог посте про glibc. Да, пока обходился без глубокого знакомства с инструментом, но знаю, что он использовался неподалёку - именно из-за сложностей с редко воспроизводящимися, но причиняющими постоянную боль багами с параллелизмом.

Далее, любопытный фрагмент истории инструмента (тут я могу быть не вполне точным, печатаю без проверки каждого фактоида). TLA+ разработал Лесли Лампорт (автор среди прочего LaTeX, но в данной истори более кстати тот факт, что он автор алгоритма distributed consensus под названием Paxos). Ему нужно было средство автоматической верификации алгоритмов такого рода и, скорее всего, ни одно из существующих на тот момент не подходило. Реализовав инструмент, он написал книгу, поясняя, как им пользоваться (также, как до этого написал книгу про LaTeX). Другой автор продолжил (хоть инструмент старый, недавняя книга вышла в 2018-м). Короче, я решил хотя бы шапочно познакомиться с TLA+, буду готов, если прийдётся применять (ну и, интересная тема - в школе и институте приходилось формально корректно доказывать теоремы на зачёт / сдачу экзамена).

Давайте для проверки тезиса, что алгоритм (довольно базовый - что может быть базовей достижения консенсуса в распределнной системе, да?) такого класса не будет воспринят серьёзно без формальной верификации, рассмотрим более недавнюю альтернативу Paxos-у, Raft. Да, быстро нашлась статья по верификации Raft-а - верифицировали с использованием Coq, кстати (как в работе автора статьи, в комментариях к которой мы находимся).

Нужно ли это программистам в повседневной работе? Хорошо, если нет! Но, когда всё-таки "не повезло" и таки-нужно, улучшение инструментов не помешает.

Вспомнился другой случай, более прозаический - баг в стандартном алгоритме сортировки Python, TimSort (используемом также в Java-системах, в стандартных коллекциях). Баг существовал в коде алгоритма (портированного вместе с багом для Java) лет 12-13, прикидочно. Его нашли и исправили только тогда, когда computer scientist-ы (уровня аспирантов, я так понимаю) попробовали верифицировать алгоритм (в рамках более крупного проекта верификации стандартных библиотек Java). На Хабре были заметки про это, вот, например (переводная): https://habr.com/ru/post/251751/. Баг, скорее всего, проявлялся довольно редко. Тем не менее, лучше всё-таки, если он исправлен, и корректность исправленного алгоритма, наконец, доказана? Это подводит к тезису, что доказывать корректность стабильных базовых библиотек (в частности, реализующих высокоуровневые примитивы для многопоточного программирования) - неплохая идея.

Насколько это дорого? Учёные делают это "за кредиты" (статьи), и при этом улучшают инструменты (о чём статья, в комментариях к которой мы находимся). Иногда за инструмент приходится браться программисту (пример - случай с glibc). Дороговизну применения инструмента в конкретном случае следуют делить на массовость применения проверяемого алгоритма с учётом возможной высокой стоимости его отказа. Иногда знаменатель оказывается достаточно большой.

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

Всего доброго.

Что вы, наличие более простого способа решения проблемы - всегда хорошая новость! Жаль, никто не смог в данном случае этого сделать... Так что, увы, хорошей новости не случилось.

В этом случае (deadlock баг в реализации conditional variable в glibc), похоже, простой способ почему-то не сработал. Сложный - сработал. И это не единственный случай, когда для решения реальной проблемы пришлось прибегнуть к сложному способу.

Для меня это (необходимость применения в некоторых сложных случаях сложных способов) - не новость. Для вас - плохая новость?

Я не хотел бы ввязываться в жаркий спор о рантайме Go, с которым я не знаком и не собираюсь знакомиться в ближайшее время. Тем не менее, считаю беседу достаточно содержательной для добавлении ещё одной реплики.

Я бегло просмотрел информацию про "go test -race" - вещь, безусловно, полезная и доступная - однако, насколько я понял, она хороша настолько, насколько хорошо тестовое покрытие. Да, документация с осторожностью рекомендует возможность запускать каких-то случаях (поиск production багов, не пойманных тестами?) и в production - предупреждая, что "The cost of race detection varies by program, but for a typical program, memory usage may increase by 5-10x and execution time by 2-20x."

Насчёт баланса стоимости и эффекта статического анализа: просмотрел довольно объёмную статью про использование MC-анализа для real world проблемы (не вникая в детали - я сам TLA+ не использовал, знаком с этим инструментом вприглядку - но кое-где не очень далеко от моего места работы этот инструмент содержательно использовался). Идёт речь о ловле и исправлении deadlock бага в реализации condition variable в стандартной библиотеке C. Может, вам тоже будет любопытно просмотреть. Стандартная библиотека C - довольно критически важная и стабильная база кода (впрочем, я подозреваю, что корректность реализации mutex.go тоже довольно критически важна, а последнее изменение в mutex.go было внесено 3 года назад). Статья годичной давности: Using TLA+ in the Real World to Understand a Glibc Bug | Probably Dance. Автор, скорее всего, работает в геймдеве, и с проблемой работал не из досужего интереса.

Ну вот, 226 строк низкоуровневого Go плюс внизу 6 строк на ассемблере - проверены вручную или формально, разница существенна. Про смайлик - в очном разговоре иронию легче уловить благодаря интонации и мимике, на письме (особенно в общении с случайным собеседником) - сложнее; да, смайлик я пропустил.

Автору - спасибо, интересно - про GenMC раньше не слышал (последний раз искал по теме - нашлись книги по TLA+ и Alloy, довольно старые системы).

Скорее, MC полезен для авторов библиотек - тех самых высокоуровневых примитивов. Угадайте, что внутри реализаций высокоуровневых примитивов? Код на Go, высокоуровневые примитивы, вы думаете? И так turtles all the way down? Я так не думаю.

Про range loop можно прочитать, например, здесь: https://docs.microsoft.com/ru-ru/cpp/cpp/range-based-for-statement-cpp?view=msvc-160 - этот синтаксис более удобен в большинстве случаев при итерировании по контейнерам.

Интересно, спасибо. Однако, не могу удержаться от того, чтобы оставить здесь пару рекомендаций.

При просмотре кода бросилось в глаза использование оператора new (второй фрагмент, класс Vertice) - подумал, что, если рассматривать этот фрагмент автономно, он может быть правильным только при использовании какой-либо библиотеки сборки мусора. Однако, потом нашёл разъяснение в этой фразе:

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

Послушайте, ничего помнить не нужно было бы, если бы вы пользовались не C указателем на ребро, а std::unique_ptr - и неявный автоматический деструктор, не удлинняя кода, избавил бы от забот; код был бы правилен без дополнительный пояснений о том, что кое-что важное опущено и что о чём-то нужно помнить. Вместо "сырого" new рекомендуется использовать std::make_unique - и воспросов в голове читателя не возникает, всё понятно. Явное использование delete становится ненужным, код упрощается.

Перескакиваю в конец, к breadthPassCommon - здесь история с тем, кто чем владеет, более запутанная. Visitor передаётся как сырой C указатель, в конце функции выясняется, что владение этим объектом тоже передано (вызывается delete, и ещё владение отслеживается хитрой логикой с флагом visitotPassed)! Использование std::unque_ptr здесь тоже бы выручило, сделало бы код менее запутанным, легче читаемым и поддерживаемым, хитрая логика и флаги были бы заменены перемещением std::unique_ptr (явно указывающим на перемещение владения).

Пожалуйста также, обратите внимание на синтаксис range loop ( for (auto&& edge : *vertice->getEdges()) ). Я лично стараюсь указатели использовать как можно реже, помогает в долгой перспективе.

У меня ровно сейчас, к сожалению, нет времени проверить легкодоступные статистические анализаторы, но, кажется, проверка на отсутствие модификации локальной переменной (если нет модификаций - диагностика "добавьте const!") может существовать, она технически нетрудна. Обращу на это внимание между делом - но в моём текущем проекте всё-таки практика применять const только в public APIs. Спасибо за дискуссию! По существу - продолжайте, пожалуйста, такие публикации, полезное для сообщества дело!

Нет у меня претензий к названиям переменных (мне даже в голову не пришло бы начинать разговоры на эту тему). Я рад, что мой длинный комментарий показался вам полезным. Не уверен, что вы сочтёте ещё один относительно длинный комментарий ниже применимым в рамках текущей беседы; печатаю его, опять же, чтобы ещё немного порефлексировать.

Моё отношение к coding guidelines довольно прагматично. Я открыт к кодификации в coding guidelines сложившегося по тем или иным причинам стиля, но мне важна внутренняя непротиворечивость такого стиля. Обсуждая с коллегами аспекты вроде рассматриваемого здесь (про желательность / нежелательность спецификатора const при объявлении локальных переменных: параметры -- локальные переменные в реализации функции) я выясняю их мотивы и подталкиваю к рефлексии на тему, насколько логичны их привычки? В случае с вашей привычкой некая поименованная величина в теле функции может оказаться констатной только из-за того, что она -- параметр, а не локальная переменная. Вы не находите сочетание строгости в отношении одних локальных переменных (параметров функции) и расслабленность в отношении других локальных переменных (не-параметров) нелогичным?

Я несколько лет назад задал самому себе такой вопрос и решил, что у меня, как у сторонника логичных самосогласованных практик, два варианта -- писать const по возможности везде (в том числе при объявлении локальных переменных и да, членов классов -- во имя самосогласованности данного правила) или писать const только там, где этот спецификатор реально детализирует контракт (как в типах параметров const T&). Порефлексировал и принял выбор в пользу второго варианта. Подтверждение нахожу в стандартной библиотеке и других пакетах, коде коллег -- поэтому удивился, увидев типа параметров const T у вас (для меня это -- ухудшение читабельности, глаз запнулся), после чего напечатал предыдущий длинный комментарий.

Упомянутый вами случай с shared_ptr, конечно, прискорбен (я, безусловно, предпочитаю unique_ptr и проработку дизайна компонент так, чтобы необходимости использования shared_ptr не было). Опять же, порефлексировав некоторое время назад, я пришёл к практике инкапсуляции деталей реализации сколь-либо сложного класса в pimpl (последнее время использую zero-cost "fast pimpl" после того, как написал поддерживающий fast impl темплейтный код, до этого использовал pimpl на unique_ptr) и запрете (= delete;) у него copy costructor, copy assignment operator. Тогда передача по значению такого сколь-нибудь сложного класса оказывается намеренно запрещена (чтобы не платить за копирование), для использования класса в функции передаётся const T& (отлично) или T& (что сигналит в сигнатуре-контракте о мутациях параметра, выполняемых в функции, о коде становится сложнее рассуждать -- цена за предполагаемую или доказанную замерами оптимизацию производительности... лучше бы доказанную). Просто T -- передача по значению (отличный выбор для лёгких "скалярных" типов, небольших struct-ов, POD). Добавление в этом случае const T -- ненужный шум в контракте и нелогичное ограничение на стороне реализации функции (нелогичное - при использовании достаточно широко принятой практике не вставлять const в объявление почти каждой локальной переменной, это и ваша практика тоже).

Возвращаясь к coding guidelines. С другой стороны, я не хочу фиксировать в coding guidelines чьи-то (мои в том числе) идеосинкразии, обусловленные индивидуальным профессиональным опытом (как ваша печальная история с shared_ptr внутри переданного по значению параметра). Вместо этого я пытаюсь отслеживать тренды в развитии языка, пытаюсь делать coding guidelines, к которым прикладываю руки, как можно более future-proof. С++ Core Guidelines -- хороший источник информации на этот счёт, на мой взгляд. Движение в сторону более частого использования практик функционального программирования (пример из Core Guidelines: "F.8: Prefer pure functions") прослеживается и там, и в современных UI библиотеках, и в практиках безопасного многопоточного программирования, поэтому я стараюсь сам и подталкиваю коллег использовать типы параметров T и const T& - избегая T& (предпочитая стиль чистых функций с немутирующими аргументами и возвратом всего через результат -- пока выигрыш в производительности от in-out стиля с T& не доказан замерами).

Вдогонку добавлю (заглянув в другую ветку комментариев) про преимущество static_cast. Для меня очевидное преимущество -- в том, что такая практика согласована с C++ Core Guidelines, конкретно -- с "ES.49: If you must use a cast, use a named cast".

"Я привык ставить const в аргументах, чтобы только по заголовку было понятно, что есть вход, что есть выход. Конечно же, никаких особых преимуществ это для аргумента get(i, j) не даёт (кроме читабельности)."

Отличная мотивация, а для меня - удобный момент снова разобрать элементы общих рекомендации по типам аргументов функций, заглянув в раздел Functions C++ Core Guidelines (и конкретно пересмотрев рекомендации F.15 и F.16).

Начнём с того, что наиболее простой способ передачи данных в функцию и получения результатов - передача по значению (тип аргумента T) и получение всех выходов функции только в возвращаемом значении (результате) - который при множественности результатов становится композитным, например, пара, кортеж (tuple) или простая структура (лучший вариант, на мой взгляд, благодаря содержательным названиям полей). Современный C++ предоставляет удобную деконструкцию композитного результата в вызывающем контексте, нампример auto [ x, y, z ] = get_coordinates(a); (этот приём используется в фрагментах кода к C++ Core Guidelines I.10, F.21, ES.10, ES.11, ES.20).

Недостаток: если тип T - достаточно крупного размера, копирование значения при передаче по значению может сказаться на производительности (что всегда не мешает проверить замерами). В этом случае помогает ссылка на константное значение, const T& - заметьте присутствие '&'. Только при наличии & появляется риск изменить внутри функции внешнее значение, и const помогает это предотвратить, а также просигналить о намереньях в сигнатуре функции: этот const T& параметр - входной, с той же подразумеваемой семантикой, как передача по значению (просто T), а ссылочность заведена для оптимизации производительности.

Поставив const T (без '&') при передаче по значению вы не меняете семантику отношений вызывающего кода с функцией - и так и так параметр, передаваемый по значению, только входной (in). В этом случае const ограничивает набор действий с параметром в реализации функции (вы не сможете обновить его значение, это сходно константным локальным переменный вроде sprite_w и sprite_h в одном из фрагментов вашего кода. Лично я рассматриваю такую типизацию как неполезную - добавляет ненужный шум в сигнатуру, накладывает ограничение на реализующий функцию код (иногда приводящее к более длинной и менее читабельной форме этого кода).

При передаче по ссылке ('&') спецификатор const действительно важен! Без него функция имеет право поменять значение переданной переменной в вызывающем контексте, таким образом параметр становится входным/выходным (in-out, описанный в рекомендации F.17) -- чего лично я стараюсь избегать, предпочитая все выходы функции возвращать в результате.

Кроме этого усложнения передача по ссылке может привнести подводные камни, так что прибегать к ней имеет смысл только убедившись, что копирование крупного значения T действительно влияет на производительность. Здесь я имею в виду возможные проблемы с aliasing. При получинии параметра const T& код функции может резонно положиться на соглашение, что данный параметр - входной, его значение в момент вызова функции таковым и остаётся до момента выхода из функции. Однако, если функция обращается к другим функциям, имеющим (другим путём) доступ к той же переменной в вызывающем контексте, это значение неожиданно может поменяться. С этой проблемой можно бороться, предотвращая aliasing с помощью &&, но, увы, такой подход усложняет код.

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

(вы уже второй раз помещаете новый комментарий в корень обсуждения :) )
Исправляю догадку, теперь сделав аналитические выкладки с рекурсивным вычислением величины от n. Теперь у меня получается Hn, частичная сумма первых n элементов гармонического ряда. Асимптотическая формула есть, log n + 0.5772... (постоянная Эйлера-Маскерони). Посчитаю моделированием для проверки - в следующий раз. Рекурсивные выкладки: данный шнурок с вероятностью 1/n добавляет новую петлю, а с вероятностью (n-1)/n не добавлет петлю к ситуации с n-1 шнурками. После приведения получаем X(n) = 1/n + X(n-1). Кажется, у меня есть на полке книга, в которой я мог бы подсмотреть похожие рассуждения - в одном из трёх томов "Искусства программирования" Кнута попадалось. Сейчас должен заняться другими вопросами - буду проверять тред на днях, не появилось ли что-либо от вас.

Information

Rating
Does not participate
Registered
Activity