Является ли дублирование кода необходимым злом в C?

16

Я довольно новичок в C, и мне интересно, является ли дублирование кода необходимым злом, когда речь идет о написании общих структур данных и C вообще?

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

Являются ли универсальные реализации нормой, или вы пишете разные реализации для каждого варианта использования?

Gerstmann
источник
11
Моральное затруднение, которое вы описываете, не является уникальным для C. Неожиданно, я считаю, что планка для создания универсальной реализации довольно высока, если у вас нет аудитории для этого. Количество усилий, необходимых для создания хорошей универсальной реализации, намного превышает точечное решение, IME.
Роберт Харви
1
@kevincline: Хорошо, у Java действительно есть дженерики. Конечно, это тип генериков «не обращай внимания на человека за кулисами».
Роберт Харви
1
А перед генериками вы могли бы передать Object и передать Float, Double, Integer и Long соответственно.
Даниэль Каплан
3
Дежа вю - это проверка запахов кода, и предпочтение DRY лежит в основе всех лучших практик, на которые стоит обратить внимание IMO. Но я выстрелил себе в ногу в ООП-ориентированных языках, слишком стараясь избежать дублирования ради самого себя. Если вы не привязываете друг к другу несвязанные проблемы без необходимости или разрушаете разборчивость ради того, чтобы две одинаковые функции стали одной, я бы использовал инстинкт в большинстве языков.
Эрик Реппен

Ответы:

27

С затрудняет написание общего кода. В отличие от C ++, который предоставляет вам шаблоны и виртуальные функции, C имеет только 3 механизма для написания универсального кода:

  1. void* указатели
  2. Макросы препроцессора
  3. Функциональные указатели

void* указатели далеки от идеала, так как вы теряете всю безопасность типов, обеспечиваемую компилятором, что может привести к трудно отлаживаемому неопределенному поведению в результате неправильных приведений типов.

Макросы препроцессора имеют хорошо отмеченные недостатки: расширение препроцессора - это просто механизм поиска / замены, который происходит перед фазой компиляции, что также может привести к трудным для отладки ошибкам. Архетипическим примером является что-то вроде:, #define add(x) (x+x)где xможет быть увеличено в два раза, если вы позвоните add(i++). Вы можете написать универсальный код в стиле шаблона полностью, используя C-макросы, но результат действительно отвратительный и сложный в обслуживании .

Указатели на функции обеспечивают хороший способ написания универсального кода, но, к сожалению, они не обеспечивают универсальность типов - они просто предоставляют возможность полиморфизма во время выполнения (именно поэтому, например, стандартной библиотеке по- qsortпрежнему требуется функция, которая принимает void*указателей.)

Вы также можете реализовать иерархии классов в C, используя структуры, как это делается в библиотеке GLib, которая предоставляет GObjectбазовый базовый класс. Но это страдает от тех же проблем, что и при использовании void*указателей, поскольку вам все еще нужно полагаться на потенциально небезопасное ручное приведение к повышению и понижению.

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

Чарльз Сальвия
источник
1
Можно также сгенерировать повторяющийся код C, используя внешние языки шаблонов / инструменты или сценарий оболочки или используя ant или ...
Job
Что это за обфусцированный Google русский URL, который вы поместили в своем ответе? При нажатии на него в моем браузере появляется «открытая» подсказка. Это безопасно?
Роберт Харви
2
@RobertHarvey Мои антивирусные программы и проверки безопасности считают, что все в порядке. Это заголовочный файл на языке C с расширением .h. Это кажется законным.
maple_shaft
@maple_shaft: ОК. Я удалил Googlization вокруг ссылки.
Роберт Харви
Это просто текстовый файл .h (C-header)
Чарльз Сальвия,
15

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

  1. Пишите для чего нужно. Общность может прийти позже, если это необходимо.
  2. Если требуется универсальность, пустые указатели, функциональные указатели и размер структуры могут быть неоценимыми. Например, подпрограмма qsort () использует все три.
  3. Дайте вес, чтобы сделать ваш код понятным.
Спарки
источник
5

Стоит отметить, что вам может не потребоваться полностью универсальное решение для уменьшения дублирования кода. Иногда достаточно обычного рефакторинга и небольшого обобщения метода.

При написании обобщенного решения для широкой аудитории вы должны учитывать:

  1. Какими новыми способами потребитель мог бы использовать ваш код, и как вы их приспособили?
  2. Какие ошибки мне нужно отлавливать? Какие ошибки я не должен ловить?
  3. Насколько надежным должен быть API? Сколько перегрузок мне нужно обеспечить?
  4. Какие меры безопасности я должен принять, чтобы код не мог использоваться в злых целях?

Эрик Липперт написал целую статью об одном дизайнерском решении в коде платформы .NET. В общем, часто проще просто рефакторинг.

Роберт Харви
источник
3

Я обычно использую обобщенную реализацию, такую ​​как glib , тогда, если приведение окажется слишком раздражающим, создайте для него небольшую специфичную для типа обертку. Тем не менее, в C ожидается много приведений, как и использование a void*в качестве универсального типа, так что то, что на другом языке можно считать «грязным», является просто типичным C. Достаточно Scary, это будет выглядеть намного более естественно, когда вы получить больше опыта в языке.

Карл Билефельдт
источник
2
«То, что на другом языке можно считать« грязным », является просто типичным C. Достаточно пугающе, это будет выглядеть намного более естественно, когда вы приобретете больше опыта в этом языке». Ха-ха-ха! Си всегда был одним из моих любимых языков, но это все еще довольно забавно!
ГленПетерсон
0

Ваша проблема повторного использования не уникальна для C. Однако, есть некоторые отличные и очень многократные реализации HashMap, на которых вы могли бы основываться. Тот, который существует уже десять или два года, находится на Java: http://docs.oracle.com/javase/7/docs/api/java/util/HashMap.html

Все Java-коллекции, включая HashMap, переписываются для Java 8, чтобы избавиться от идеи итератора, и вместо этого требуют, чтобы вы передали функцию в коллекцию, чтобы она могла выполнять внутренние итерации. Это большой выигрыш для написания параллельного кода - коллекция может управлять параллельным внутренним процессом, поэтому клиенту это не нужно. Тем не менее, большинство программистов не привыкли передавать указатели на функции. Это хорошо объясняет, куда движется HashMap: http://cr.openjdk.java.net/~briangoetz/lambda/collections-overview.html

Синтаксис Scala может затруднить перевод чего-либо подобного в C, но вот пример коллекции, которая уже работает так, как будет работать коллекции Java 8 - по умолчанию неизменяемая, предоставляя различные методы преобразования, используя указатели функций и другие классные вещи, такие как частичное применение: http://www.scala-lang.org/api/current/index.html#scala.collection.Map

Если вы делаете много вещей с HashMaps и другими более продвинутыми структурами данных, вы можете захотеть взглянуть на язык, который их поддерживает.

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

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

GlenPeterson
источник
1
- «Я не думаю, что слишком упрощенно говорить, что всякий раз, когда вы обнаруживаете, что дублируете код, вы должны создавать функцию», - в C это слишком упрощено. Попробуйте создать стек в C для целых, двойных и строковых значений без дублирования функций push и pop для каждого типа данных.
mike30
@mike Разве это не возможно, используя пустые указатели?
Сакиск
@mike - хорошая мысль. Но в противоположность этому вы можете реализовать Stack для каждого типа данных таким образом, чтобы вам не нужно было снова внедрять Stack. Так что цель не в numberOfImplementations = 1, а в большей степени как 5. Возможно, вы сможете заставить intреализацию работать для chars ... Прошло так много времени с тех пор, как я использовал C, и я не могу вспомнить.
ГленПетерсон
1
@faif. Да, но необходимость использования пустых указателей делает точку для C. Приведение указателей медленнее и менее элегантно, чем альтернативы, найденные в C ++. Дублирование кода занимает несколько более высокое место в списке возможных вариантов при кодировании на C.
mike30
@ mike30 Существуют ли какие-либо доказательства / ссылки на тему: «Приведение указателей в C медленнее, чем в C ++?»
Сакиск
0

Мне интересно, если дублирование кода является необходимым злом, когда речь идет о написании общих структур данных и C в целом?

В C, абсолютно для меня, как человека, который прыгает между C и C ++. Я определенно повторяю больше тривиальных вещей ежедневно в C, чем в C ++, но намеренно, и я не обязательно вижу это как «зло», потому что есть по крайней мере некоторые практические преимущества - я думаю, что это ошибка, чтобы рассмотреть все вещи Строго говоря, «добро» или «зло» - практически все зависит от компромиссов. Четкое понимание этих компромиссов является ключом к тому, чтобы не избегать прискорбных решений в ретроспективе, а просто маркировать вещи как «добро» или «зло», как правило, игнорирует все такие тонкости.

Хотя эта проблема не является уникальной для C, как отмечали другие, она может быть значительно более усугублена в C из-за отсутствия чего-либо более элегантного, чем макросы или пустые указатели для обобщений, неловкости нетривиального ООП и того факта, что Стандартная библиотека C не поставляется с контейнерами. В C ++ человек, реализующий свой собственный связанный список, может получить злобную толпу людей, требующих, почему они не используют стандартную библиотеку, если они не студенты. В C вы бы пригласили злобного моба, если не можете уверенно развернуть элегантную реализацию связанного списка во сне, поскольку программист на C часто ожидает, что, по крайней мере, сможет делать такие вещи ежедневно. Это' Не из-за какой-то странной одержимости связанными списками Линус Торвальдс использовал реализацию поиска и удаления SLL с использованием двойной косвенности в качестве критерия для оценки программиста, который понимает язык и имеет «хороший вкус». Это потому, что программисты на Си могут быть обязаны реализовывать такую ​​логику в своей карьере тысячу раз. В этом случае для C это похоже на то, как шеф-повар оценивает навыки нового повара, заставляя их просто приготовить несколько яиц, чтобы убедиться, что они хотя бы овладеют базовыми вещами, которые им придется делать все время.

Например, я, вероятно, реализовал эту базовую структуру данных «свободный индексированный список» в дюжине раз на C локально для каждого сайта, который использует эту стратегию распределения (почти все мои связанные структуры, чтобы избежать выделения одного узла за раз и вдвое сократить объем памяти). Стоимость ссылок на 64-битную):

введите описание изображения здесь

Но в C это просто занимает очень небольшой объем кода для reallocрастущего массива и выделяет из него часть памяти, используя индексированный подход к свободному списку при реализации новой структуры данных, которая использует этот.

Теперь я реализовал то же самое в C ++, и там он реализован только один раз как шаблон класса. Но это намного более сложная реализация на стороне C ++ с сотнями строк кода и некоторыми внешними зависимостями, которые также охватывают сотни строк кода. И главная причина, по которой это намного сложнее, заключается в том, что мне приходится кодировать его против идеи, что это Tможет быть любой возможный тип данных. Это может произойти в любой момент времени (за исключением случаев уничтожения, что я должен сделать явно, как со стандартными контейнерами библиотеки), мне пришлось подумать о правильном выравнивании, чтобы выделить память дляT (хотя, к счастью, это стало проще в C ++ 11 и далее), это может быть нетривиально конструируемым / разрушаемым (требует размещения новых и ручных вызовов dtor), я должен добавить методы, которые понадобятся не всем, а некоторым, и я должен добавить итераторы, и изменяемые и только для чтения (const) итераторы, и так далее, и так далее.

Растущие массивы не ракетостроение

В C ++ люди говорят, что std::vectorэто работа ученого-ракетчика, оптимизированного до смерти, но он не работает лучше, чем динамический массив C, закодированный для определенного типа данных, который просто используется reallocдля увеличения емкости массива при откате с помощью дюжина строк кода. Разница в том, что требуется очень сложная реализация, чтобы сделать растущую последовательность произвольного доступа полностью совместимой со стандартом, избегать вызова ctors на не вставленных элементах, исключать исключения, предоставлять как постоянные, так и неконстантные итераторы произвольного доступа, использовать тип черты для устранения неоднозначности ctors заполнения от ctors диапазона для определенных целочисленных типовTпотенциально обрабатывать POD по-разному, используя черты типа и т. д. и т. д. и т. д. В этот момент вам действительно нужна очень сложная реализация просто для создания растущего динамического массива, но только потому, что он пытается обрабатывать все возможные варианты использования, какие только можно вообразить. С другой стороны, вы можете получить массу преимуществ от всех этих дополнительных усилий, если вам действительно нужно хранить как POD, так и нетривиальные UDT, использовать универсальные алгоритмы на основе итераторов, которые работают с любой совместимой структурой данных, извлекайте выгоду из обработки исключений и RAII, по крайней мере, иногда переопределяйте std::allocatorс помощью собственного распределителя и т. д.std::vector было на весь мир людей, которые использовали его, но это для чего-то реализованного в стандартной библиотеке, предназначенной для удовлетворения потребностей всего мира.

Более простые реализации, обрабатывающие очень специфические случаи использования

В результате простой обработки очень специфических сценариев использования с моим «индексированным списком свободных мест», несмотря на то, что десятки раз реализован этот свободный список на стороне C и в результате получился дублирующий некоторый тривиальный код, я, вероятно, написал меньше кода Всего в C было реализовано это в дюжину раз, чем мне пришлось реализовать это всего один раз в C ++, и мне пришлось потратить меньше времени на поддержку этих дюжин реализаций C, чем мне пришлось поддерживать эту одну реализацию C ++. Одна из основных причин, по которой сторона C настолько проста, заключается в том, что я обычно работаю с POD в C всякий раз, когда использую эту технику, и обычно мне не нужно больше функций, чем insertиeraseна конкретных сайтах, в которых я реализую это локально. По сути, я могу просто реализовать самое маленькое подмножество функциональных возможностей, предоставляемых версией C ++, поскольку я могу делать гораздо больше предположений о том, что я делаю, и мне не нужен дизайн, когда я реализую его для очень специфического использования. кейс.

Теперь версия C ++ намного удобнее и безопаснее в использовании, но она по-прежнему была основной PITA для реализации и обеспечения исключительной и двунаправленной совместимости с итераторами, например, таким образом, что разработка одной общей, многократно используемой реализации, вероятно, стоит больше времени, чем это реально экономит в этом случае. И многие из этих затрат на его реализацию в обобщенном виде тратятся не только заранее, но многократно в виде таких вещей, как увеличение времени сборки, выплачиваемое снова и снова каждый день.

Не атака на C ++!

Но это не атака на C ++, потому что я люблю C ++, но когда дело доходит до структур данных, я предпочел C ++ главным образом за действительно нетривиальные структуры данных, которые я хочу потратить гораздо больше времени на предварительную реализацию в очень обобщенный способ, сделать исключительную Tзащиту от всех возможных типов , сделать стандартную и повторяемую, и т. д., где этот тип первоначальной стоимости действительно окупается в виде тонны пробега.

Тем не менее, это также способствует совершенно другому дизайну мышления. В C ++, если я хочу создать Octree для обнаружения столкновений, я склонен обобщать его до n-й степени. Я не просто хочу хранить в нем индексированные треугольные сетки. Почему я должен ограничивать его одним типом данных, с которым он может работать, когда у меня под рукой есть сверхмощный механизм генерации кода, который исключает все штрафы за абстракцию во время выполнения? Я хочу, чтобы он хранил процедурные сферы, кубы, воксели, NURB-поверхности, облака точек и т. Д. И т. Д. И старался, чтобы это было хорошо для всего, потому что соблазнительно захотеть создать его таким образом, когда у вас под рукой есть шаблоны. Возможно, я даже не хочу ограничивать это обнаружением столкновений - как насчет трассировки лучей, выбора и т. Д.? C ++ делает его на первый взгляд "просто" обобщить структуру данных в n-й степени. И вот как я использовал для разработки таких пространственных индексов в C ++. Я пытался спроектировать их так, чтобы они отвечали всем потребностям голода в мире, и в обмен я получил, как правило, «мастер на все руки» с чрезвычайно сложным кодом, чтобы сбалансировать его со всеми возможными вариантами использования, которые только можно вообразить.

Как ни странно, однако, я получил больше повторного использования из пространственных индексов, которые я реализовал в C на протяжении многих лет, и я не виноват в C ++, а только в том, что язык соблазняет меня. Когда я кодирую что-то вроде октри в C, я имею тенденцию просто заставить его работать с точками и быть довольным этим, потому что язык делает слишком трудным даже пытаться обобщить его до n-й степени. Но из-за этих тенденций я склонялся к тому, чтобы на протяжении многих лет проектировать вещи, которые на самом деле более эффективны и надежны и действительно хорошо подходят для определенных задач, поскольку они не заботятся о том, чтобы быть общими для n-й степени. Они становятся тузами в одной специализированной категории, а не мастером на все руки. Опять же, это не вина C ++, а просто человеческие тенденции, которые я имею, когда использую его, а не C.

Но в любом случае я люблю оба языка, но есть разные тенденции. У КИ есть тенденция не достаточно обобщать. В C ++ я склонен слишком много обобщать. Использование обоих помогло мне сбалансировать себя.

Являются ли универсальные реализации нормой, или вы пишете разные реализации для каждого варианта использования?

Для тривиальных вещей, таких как односвязные 32-битные индексированные списки, использующие узлы из массива или массива, который перераспределяет себя (аналогичный эквивалент std::vectorв C ++) или, скажем, октодерево, которое просто хранит точки и стремится больше ничего не делать, я не не стоит обобщать до точки хранения любого типа данных. Я реализую их для хранения определенного типа данных (хотя он может быть абстрактным и в некоторых случаях использовать указатели на функции, но, по крайней мере, более специфичным, чем типирование утки со статическим полиморфизмом).

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

Для нетривиальных вещей, это обычно, когда я достигаю C ++, но если бы я должен был реализовать это в C, я бы рассмотрел использование только void*указателей, возможно, принял бы размер шрифта, чтобы знать, сколько памяти выделить для каждого элемента, и, возможно, copy/destroyуказатели на функции глубокое копирование и уничтожение данных, если они не являются тривиально-разрушаемыми. В большинстве случаев я не беспокоюсь и не использую столько C для создания самых сложных структур данных и алгоритмов.

Если вы используете одну структуру данных достаточно часто с определенным типом данных, вы также можете обернуть безопасную от типа версию поверх той, которая работает только с битами и байтами и указателями функций, и void*, например, для восстановления безопасности типов через оболочку C.

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

Хеш-таблицы довольно сомнительны, поскольку их реализация может быть тривиальной или сложной, в зависимости от того, насколько сложны ваши потребности в отношении хеш-значений, повторных хэш-операций, если вам нужно автоматически неявно увеличивать таблицу или вы можете ожидать размер таблицы в продвигаться вперед, независимо от того, используете ли вы открытую адресацию или отдельную цепочку и т. д. Но следует иметь в виду, что если вы идеально адаптировали хеш-таблицу к потребностям конкретного сайта, она часто не будет настолько сложной в реализации и часто выигрывает. не будет лишним, когда оно специально для этих нужд. По крайней мере, я оправдываюсь, если внедряю что-то локально. Если нет, вы можете просто использовать метод, описанный выше, с void*указателями и функциями для копирования / уничтожения объектов и их обобщения.

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

В качестве другого примера, я часто находил, что чрезвычайно легко внедрять решения, которые в 10 раз быстрее или больше, чем решения VFX, предлагаемые Pixar или Dreamworks. Я могу сделать это во сне. Но это не потому, что мои реализации превосходят - далеко не так. Они совершенно уступают большинству людей. Они превосходят только мои очень специфические варианты использования. Мои версии гораздо менее применимы, чем Pixar или Dreamwork. Это смехотворно несправедливое сравнение, потому что их решения абсолютно блестящие по сравнению с моими простыми решениями, но в этом-то и дело. Сравнение не должно быть справедливым. Если все, что вам нужно, это несколько очень специфических вещей, вам не нужно заставлять структуру данных обрабатывать бесконечный список вещей, которые вам не нужны.

Однородные биты и байты

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

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

Таким образом, одна из стратегий, которую следует использовать в C, хотя она имеет пониженное богатство типов и безопасность, состоит в обобщении на уровне битов и байтов. Вы могли бы предположить , что Dogsи Catsтребуется одинаковое количество битов и байтов, имеют одни и те же поля, один и тот же указатель на таблицу указателей функции. Но взамен вы можете затем кодировать меньше структур данных, но, что не менее важно, хранить все эти вещи эффективно и непрерывно. В этом случае вы относитесь к собакам и кошкам как к аналогичным профсоюзам (или вы можете просто использовать профсоюз).

И это приводит к огромным затратам на безопасность. Если есть одна вещь, по которой я скучаю больше всего в C, это безопасность типов. Это приближается к уровню сборки, где структуры просто указывают, сколько памяти выделено и как выровнено каждое поле данных. Но на самом деле это моя главная причина использовать C. Если вы действительно пытаетесь контролировать макеты памяти и то, где все распределяется и где все хранится относительно друг друга, часто это помогает просто думать о вещах на уровне битов и байт, и сколько битов и байтов вам нужно для решения конкретной проблемы. Там немота системы типов С может на самом деле стать полезной, а не помехой. Как правило, это приводит к гораздо меньшему количеству типов данных,

Иллюзорное / видимое дублирование

Теперь я использовал «дублирование» в широком смысле для вещей, которые могут даже не быть лишними. Я видел, как люди отличают такие термины, как «случайное / явное» дублирование от «фактического дублирования». Я вижу это в том, что во многих случаях нет такого четкого различия. Я нахожу различие больше как «потенциальная уникальность» против «потенциального дублирования», и оно может пойти любым путем. Это часто зависит от того, как вы хотите, чтобы ваши проекты и реализации развивались, и насколько идеально они будут адаптированы для конкретного случая использования. Но я часто обнаруживаю, что то, что может казаться дублированием кода, позже становится ненужным после нескольких итераций улучшений.

Возьмите простую реализацию растущего массива с использованием reallocаналога std::vector<int>. Первоначально это может быть избыточно, скажем, с использованием std::vector<int>в C ++. Но, измерив, вы можете обнаружить, что было бы полезно заранее выделить 64 байта, чтобы разрешить вставку шестнадцати 32-разрядных целых чисел без выделения кучи. Теперь это больше не избыточно, по крайней мере, не с std::vector<int>. И тогда вы можете сказать: «Но я мог бы просто обобщить это на новое SmallVector<int, 16>, и вы могли бы. Но тогда, скажем, вы найдете, что это полезно, поскольку они предназначены для очень маленьких, недолговечных массивов, чтобы в четыре раза увеличить емкость массива при распределении кучи вместо увеличивается на 1,5 (примерно столько, сколькоvectorреализации используют) при отработке предположения, что емкость массива всегда является степенью двойки. Теперь ваш контейнер действительно другой, и, вероятно, такого контейнера нет. И, возможно, вы могли бы попытаться обобщить такое поведение, добавив все больше и больше параметров шаблона для более тяжелой настройки предраспределения, настройки поведения перераспределения и т. Д. И т. Д., Но в этот момент вы можете найти что-то действительно громоздкое для использования по сравнению с дюжиной строк простого C код.

И вы можете даже достичь точки, когда вам понадобится структура данных, которая выделяет 256-битную выровненную и дополненную память, храня исключительно POD для инструкций AVX 256, предварительно выделяет 128 байтов, чтобы избежать выделения кучи для общих входных данных небольших размеров, удваивается по емкости, когда полный, и позволяет безопасно перезаписывать конечные элементы, превышающие размер массива, но не превышающие емкость массива. В этот момент, если вы все еще пытаетесь обобщить решение, чтобы избежать дублирования небольшого количества кода на C, пусть боги программирования помилуют вашу душу.

Таким образом, бывают и такие моменты, когда то, что изначально выглядит избыточным, начинает расти, когда вы приспосабливаете решение, которое лучше и лучше подходит для определенного варианта использования, во что-то совершенно уникальное и совсем не избыточное. Но это только для вещей, где вы можете позволить себе идеально адаптировать их к конкретному случаю использования. Иногда нам просто нужна «приличная» вещь, обобщенная для наших целей, и там я получаю наибольшую пользу от очень обобщенных структур данных. Но для исключительных вещей, идеально сделанных для конкретного случая использования, идея «общего назначения» и «сделанная идеально для моих целей» начинает становиться слишком несовместимой.

PODы и Примитивы

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

Простой пример - интернирование коротких строк (как это обычно бывает со строками, используемыми для ключей поиска - они, как правило, очень короткие). Зачем беспокоиться о работе со всеми этими строками переменной длины, размеры которых меняются во время выполнения, что подразумевает нетривиальные конструкции и разрушения (поскольку нам может потребоваться куча выделения и освобождения)? Как насчет того, чтобы просто хранить эти вещи в центральной структуре данных, например, в поточно-ориентированном дереве или в хеш-таблице, предназначенной только для интернирования строк, а затем ссылаться на эти строки с помощью простого старого int32_tили:

struct IternedString 
{
    int32_t index;
};

... в наших хеш-таблицах, красно-черных деревьях, списках пропусков и т. д., если нам не нужна лексикографическая сортировка? Теперь все наши другие структуры данных, которые мы кодировали для работы с 32-разрядными целыми числами, теперь могут хранить эти встроенные строковые ключи, которые фактически являются просто 32-разрядными ints. И я нашел, по крайней мере, в моих случаях использования (возможно, это просто моя область, так как я работаю в таких областях, как трассировка лучей, обработка сетки, обработка изображений, системы частиц, привязка к языкам сценариев, реализации низкоуровневых многопоточных графических интерфейсов и т. Д. - низкоуровневые вещи, но не такие низкоуровневые, как ОС), когда код по совпадению оказывается более эффективным и простым, просто сохраняя индексы для таких вещей. Это делает меня таким, что я часто работаю, скажем, 75% времени, просто int32_tиfloat32 в моих нетривиальных структурах данных, или просто хранить вещи одинакового размера (почти всегда 32-битные).

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

Тестирование и надежность

И последнее, что я хотел бы предложить, и это может быть не для всех, - отдать предпочтение написанию тестов для этих структур данных. Сделайте их действительно хорошими в чем-то. Убедитесь, что они ультра надежные.

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

Мое чувство эстетики изменилось за эти годы. Я больше не раздражаюсь, потому что вижу, как одна библиотека реализует точечный продукт или какую-то тривиальную логику SLL, которая уже реализована в другой. Я раздражаюсь только тогда, когда дела плохо проверены и ненадежны, и я обнаружил, что это гораздо более продуктивное мышление. Я искренне имел дело с основами кода, которые дублируют ошибки через дублированный код, и видел худшие случаи кодирования с копированием и вставкой, из-за которых то, что должно было быть тривиальным изменением одного центрального места, превращалось во подверженное ошибкам каскадное изменение для многих. Тем не менее, во многих случаях это было результатом плохого тестирования, когда код не стал надежным и хорошим в том, что он делал в первую очередь. Раньше, когда я работал с ошибочными устаревшими кодами, мой разум связывал все формы дублирования кода с очень высокой вероятностью дублирования ошибок и необходимости каскадных изменений. Тем не менее, миниатюрная библиотека, которая делает одну вещь чрезвычайно хорошо и надежно, найдет очень мало причин для изменения в будущем, даже если она имеет некоторый избыточно выглядящий код здесь и там. Мои приоритеты были тогда, когда дублирование раздражало меня больше, чем плохое качество и отсутствие тестирования. Эти последние вещи должны быть главным приоритетом.

Дублирование кода для минимализма?

Это забавная мысль, которая возникла у меня в голове, но рассмотрим случай, когда мы можем столкнуться с библиотекой C и C ++, которая примерно выполняет одну и ту же функцию: обе имеют примерно одинаковую функциональность, одинаковую обработку ошибок, одна из них незначительна более эффективны, чем другие, и т. д. И самое главное, оба они грамотно реализованы, хорошо протестированы и надежны. К сожалению, я должен говорить здесь гипотетически, так как я никогда не находил ничего близкого к идеальному параллельному сравнению. Но самые близкие вещи, которые я когда-либо обнаруживал в этом параллельном сравнении, часто имели библиотеку C, намного меньшую, чем эквивалент C ++ (иногда 1/10 его размера кода).

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

На днях я смотрел на чей-то raytracer на github, и он был реализован на C ++ и требовал так много кода для игрушечного raytracer. И я не тратил много времени на просмотр кода, но там было множество конструкций общего назначения, которые обрабатывали путь, намного больше, чем требовалось raytracer. И я признаю этот стиль кодирования, потому что раньше я использовал C ++ таким же образом, своего рода супер снизу вверх, концентрируясь на создании полноценной библиотеки структур данных очень общего назначения, которые вначале выходят далеко за рамки непосредственного проблема под рукой, а затем решить актуальную проблему второй. Но в то время как эти общие структуры могут устранить некоторую избыточность здесь и там, и они могут многократно использоваться в новых контекстах, взамен они сильно раздувают проект, обменивая небольшую избыточность с кучей ненужного кода / функциональности, и последний не обязательно легче поддерживать, чем первый. Наоборот, мне часто бывает труднее поддерживать, так как трудно поддерживать дизайн чего-то общего, который должен был бы сбалансировать проектные решения в соответствии с самым широким диапазоном возможных потребностей.


источник