Чи є дублювання коду необхідним злом у С?


16

Я досить новачок у C, і мені цікаво, чи дублювання коду є необхідним злом, коли справа стосується написання загальних структур даних та С загалом?

Я можу спробувати написати загальну реалізацію для hash mapприкладу, але я завжди вважаю кінцевий результат безладним. Я також можу написати спеціалізовану реалізацію саме для цього конкретного випадку використання, зберігати код чітким та легким для читання та налагодження. Останнє, звичайно, призведе до певного дублювання коду.

Чи загальна реалізація є нормою, чи ви пишете різні реалізації для кожного випадку використання?


11
Моральна суть, яку ви описуєте, не характерна лише для C. Анекдотично я вважаю, що планка створення загальної реалізації є досить високою, якщо у вас немає для неї аудиторії. Кількість зусиль, необхідних для створення хорошої загальної реалізації, набагато перевищує точку рішення, IME.
Роберт Харві


1
@kevincline: Ну, у Java є дженерики. Зрозуміло, це генеричні дані "не звертай уваги на людину за завісою".
Роберт Харві

1
А перед дженериками ви могли пройти в Object і пройти відповідно в Float, Double, Integer і Long.
Даніель Каплан

3
Deja vu - це перевірка запаху коду, і перевага щодо DRY лежить в основі всіх найкращих практик, на які варто звернути увагу на IMO. Але я застрелився ногами мов, орієнтованих на ООП, намагаючись занадто сильно уникати дублювання заради себе. Якщо ви не зобов'язуєте себе непотрібними стосунками один до одного або не руйнуєте розбірливості задля того, щоб дві подібні функції стали однією, я б пішов з інстинктом на більшості будь-якої мови.
Erik Reppen

Відповіді:


27

C ускладнює написання загального коду. На відміну від 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, використовуючи зовнішні мови / інструменти шаблону або скрипт оболонки або використовуючи мурашник або ...
Job

Що це за прихована від Google російська URL-адреса, яку ви розмістили у своїй відповіді? При натисканні на неї у моєму браузері з’являється «відкрита» підказка. Це безпечно?
Роберт Харві

2
@RobertHarvey Мої антивірусні та захисні програми вважають це нормально. Це файл заголовка мови С, розширення .h. Здається легітимним.
maple_shaft

@maple_shaft: Гаразд. Я видалив Гуглізацію навколо посилання.
Роберт Харві

Це просто звичайний текст .h (C-header) файлу
Чарльз Сальвія

15

Я не можу говорити за інших, але на моєму особистому досвіді роботи з C, дублювання коду не було великою проблемою. Чи це пов’язано з розмірами проекту чи зачарованим набором зразків, я не можу сказати. Однак є три основні правила, яких я дотримуюся, і я вважаю, що вони застосовні. У жодному конкретному порядку вони ...

  1. Пишіть для того, що потрібно. Загальність може з’явитися пізніше, якщо це потрібно.
  2. Якщо потрібна загальність, недійсні покажчики, покажчики функцій та розмір структури можуть бути безцінними. Наприклад, звичайний qsort () використовує всі три.
  3. Надайте вагу, щоб зробити ваш код зрозумілим.

5

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

Пишучи узагальнене рішення для широкої аудиторії, ви повинні врахувати:

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

Ерік Ліпперт написав цілу статтю про єдине дизайнерське рішення в рамковому коді .NET. Загалом, часто простіше просто рефактор.


3

Зазвичай я використовую загальну реалізацію, як, наприклад, glib's , тоді, якщо кастинг закінчується занадто дратівливим, зробіть для нього невелику специфічну обгортку. Однак в C очікується багато кастингу, як і використання a void*як родового типу, тому те, що в іншій мові вважатиметься "брудним", просто типово C. На жаль, це буде виглядати набагато природніше, коли ви отримати більший досвід мови.


2
"Те, що в іншій мові вважатиметься" безладним ", є просто типовим. На жаль. Це буде виглядати набагато природніше, коли ви отримаєте більше досвіду в мові". Ха-ха-ха! C завжди був однією з моїх улюблених мов, але це все ще досить смішно!
GlenPeterson

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 та іншими більш досконалими структурами даних, ви можете подивитися мову, яка їх підтримує.

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

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


1
- "Я не думаю, що це занадто багато спрощення, щоб сказати, що кожного разу, коли ви опинитесь дублюючим кодом, вам слід зробити функцію" - В C це надмірне спрощення. Спробуйте створити стек у C для цілих чисел, парних рядків та рядків, не дублюючи функції push та pop для кожного типу даних.
mike30

@mike Чи не можливо це за допомогою пустових покажчиків?
sakisk

@mike - хороший момент. Але протилежним моментом буде те, що ви можете реалізувати стек для кожного типу даних таким чином, що вам не потрібно буде знову реалізовувати стек. Таким чином, мета - не числоOfImplementations = 1, а більше як 5. Ви, можливо, зможете зробити intроботу по впровадженню для chars ... Пройшло так довго, як я використовував C, що не можу згадати.
GlenPeterson

1
@faif. Так, але необхідність використання недійсних покажчиків робить сенс для C. Введення покажчиків повільніше і менш елегантно, ніж альтернативи, знайдені в C ++. Дублювання коду займає дещо вище у списку можливих варіантів при кодуванні в C.
mike30

@ mike30 Чи є докази / посилання про: "Введення покажчиків у C повільніше, ніж альтернативи, знайдені в C ++?"
sakisk

0

Мені цікаво, чи є дублювання коду необхідним злом, коли справа стосується написання загальних структур даних та С загалом?

В C, абсолютно для мене, як того, хто підстрибує між C і C ++. Я, безумовно, щоденно копіюю тривіальніші речі на C, ніж на C ++, але навмисно, і я не обов'язково вважаю це "злом", оскільки є хоч якісь практичні переваги - я думаю, що помилково розглядати всі речі як суворо "добрий" чи "злий" - майже все - це питання компромісів. Чітке розуміння цих компромісів є ключем до того, щоб не уникати сумнівних рішень заднім числом, а лише маркування речей як "добрих" або "злих" взагалі ігнорує всі подібні тонкощі.

Хоча проблема не характерна для C, як вказували інші, вона може бути значно загострена в С через відсутність нічого більш елегантного, ніж макроси або недійсні покажчики на дженерики, незграбність нетривіального OOP, і те, що Стандартна бібліотека C не постачається з жодними контейнерами. У C ++ особа, яка реалізує свій власний зв’язаний список, може отримати розлючену групу людей, що вимагають, чому вони не використовують стандартну бібліотеку, якщо вони не є студентами. В C ви запросите розлюченого натовпу, якщо ви не можете впевнено розгорнути елегантне виконання пов'язаного списку уві сні, оскільки від програміста на C часто очікується, що він зможе робити такі речі щодня. Це ' s не з-за дивної нав'язливості у пов'язаних списках, що Лінус Торвальдс використовував реалізацію пошуку та видалення SLL, використовуючи подвійну непряму як критерій оцінку програміста, який розуміє мову та має "гарний смак". Це тому, що програмістам C може бути потрібно в тисячі разів реалізувати таку логіку у своїй кар'єрі. У цьому випадку для C це схоже на шеф-кухаря, який оцінює навички нового кухаря, змушуючи їх просто приготувати кілька яєць, щоб побачити, чи принаймні вони володіють основними речами, які потрібно буде робити весь час.

Наприклад, я, ймовірно, реалізував цю базову структуру даних "індексованого вільного списку" в десяток разів на локальному рівні для кожного сайту, який використовує цю стратегію розподілу (майже всі мої пов'язані структури, щоб уникнути виділення одного вузла за один раз і вдвічі менше пам'яті витрати на посилання на 64-розрядні):

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

Але в C він забирає дуже малий обсяг коду до reallocмасиву, що обробляється, і об'єднує деяку кількість пам’яті з нього, використовуючи індексований підхід до вільного списку при впровадженні нової структури даних, яка використовує цю.

Тепер у мене те саме, що реалізовано в C ++, і там я його реалізую лише один раз як шаблон класу. Але це набагато складніше реалізація на стороні C ++ із сотнями рядків коду та деякими зовнішніми залежностями, які також охоплюють сотні рядків коду. І головна причина, що це набагато складніше, полягає в тому, що я мушу кодувати це проти ідеї, яка Tможе бути будь-яким можливим типом даних. Він може кидатись у будь-який момент часу (за винятком випадків його знищення, що я маю робити явно, як зі стандартними бібліотечними контейнерами), я повинен був подумати про правильне вирівнювання, щоб виділити пам'ять дляT (хоча, на щастя, це полегшується в C ++ 11 і далі), це може бути нетривіально сконструйованим / руйнівним (вимагає розміщення нових і ручних викликів dtor), я повинен додати методи, які не все знадобляться, але деякі речі знадобляться, і мені потрібно додати ітераторів, як мутаційних, так і лише для читання (const) ітераторів, і так далі, і так далі.

Рослинні масиви не є ракетними науками

У C ++ люди здаються, що це std::vectorробота ракетного вченого, оптимізована до загибелі, але це не краще, ніж динамічний масив C, кодований проти конкретного типу даних, який просто використовує reallocдля збільшення ємності масиву на натисканнях із десяток рядків коду. Різниця полягає в тому, що потрібна дуже складна реалізація, щоб зробити просто зростаючу послідовність з випадковим доступом повністю сумісну зі стандартом, уникати посилань на цитри на невстановлених елементах, винятково безпечних, забезпечити як const, так і не const ітераторів випадкового доступу, використовувати тип ознаки для розрізнення цистерн заповнення від цистерн діапазону для певних інтегральних типівT, потенційно обробляти POD-різними, використовуючи риси типу тощо, тощо. І т. д. І тоді вам дійсно потрібна дуже складна реалізація просто для того, щоб зробити динамічний масив, що розростається, але тільки тому, що він намагається обробити всі можливі випадки використання, які коли-небудь можна уявити. З іншого боку, ви можете отримати цілий пробіг від усіх цих додаткових зусиль, якщо вам справді потрібно зберігати як POD, так і нетривіальні UDT, використовувати для загальних алгоритмів на основі ітератора, які працюють на будь-якій сумісній структурі даних, виграйте від обробки винятків та RAII, принаймні іноді переважайте std::allocatorз власним призначеним алокатором тощо. Це безумовно окупається в стандартній бібліотеці, якщо врахувати, скільки користіstd::vector було у всьому світі людей, які ним користуються, але це для чогось реалізованого в стандартній бібліотеці, розробленої для задоволення потреб усього світу.

Простіші реалізації Поводження із дуже конкретними випадками використання

Внаслідок простого оброблення дуже конкретних випадків використання з моїм "індексованим безкоштовним списком", незважаючи на те, що цей безкоштовний список було здійснено десяток разів на стороні С і в результаті було дубльовано якийсь тривіальний код, я, мабуть, написав менше коду в цілому на C, щоб реалізувати це в десяток разів, ніж мені довелося реалізувати це лише один раз у C ++, і мені довелося витрачати менше часу на підтримку цих десятків реалізацій C, ніж мені довелося підтримувати цю одну C ++ реалізацію. Однією з головних причин, на якій сторона С є такою простою, є те, що я, як правило, працюю з PODs в C коли коли я використовую цю техніку, і я, як правило, не потребую більше функцій, ніж insertіeraseна конкретних сайтах, на яких я реалізую це локально. В основному я можу просто реалізувати найсміливіший підмножина функціоналу, що надається версією C ++, оскільки я вільний робити ще багато припущень щодо того, що я роблю, і що мені не потрібно для дизайну, коли я його реалізую для дуже конкретного використання справа.

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

Не напад на C ++!

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

Але це також сприяє дуже різному дизайнерському мисленню. У C ++, якщо я хочу скласти Окрем для виявлення зіткнень, у мене є тенденція хочу узагальнити його до п ятого ступеня. Я не просто хочу, щоб він зберігав індексовані трикутникові сітки. Чому я повинен обмежувати його лише одним типом даних, з яким він може працювати, коли у мене під рукою є надзвичайно потужний механізм генерації коду, який виключає всі покарання за абстракцію під час виконання? Я хочу, щоб він зберігав процедурні сфери, кубики, вокселі, поверхні NURB, точки хмари, і т. Д. І т. Д. І ін. Я навіть не хотів би обмежувати це виявленням зіткнень - як щодо випромінювання, вибору тощо? C ++ дозволяє спочатку виглядати "сортовим" узагальнити структуру даних до n-го ступеня. І саме так я використовував для проектування таких просторових індексів у C ++. Я намагався розробити їх для вирішення потреб у голоді всього світу, і те, що я отримав в обмін, як правило, був «джеком всіх торгів» з надзвичайно складним кодом, щоб збалансувати його між усіма можливими випадками використання, які можна уявити.

Як ні дивно, я більше використовував просторові індекси, впроваджені в C протягом багатьох років, і без вини C ++, але тільки моє, в чому мова спокушає мене робити. Коли я кодую щось на зразок октрису на мові С, у мене є тенденція просто змусити його працювати з очками і бути задоволеним лише цим, оскільки мова робить занадто важким навіть намагання узагальнити його до п ятого ступеня. Але завдяки цим тенденціям я протягом багатьох років намагався розробляти речі, які насправді є більш ефективними та надійними та дійсно добре підходять для виконання певних завдань, оскільки вони не заважають бути загальними до п ятого ступеня. Вони стають тузами в одній спеціалізованій категорії замість джека всіх торгів. Знову ж таки, це не винна C ++, а просто людські тенденції, які у мене є, коли я використовую його на відміну від C.

Але я все одно люблю обидві мови, але є різні тенденції. У КІ є тенденція до недостатнього узагальнення. У C ++ у мене є тенденція до загального узагальнення. Використання обох допомогло мені збалансувати себе.

Чи загальна реалізація є нормою, чи ви пишете різні реалізації для кожного випадку використання?

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

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

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

Якщо ви використовуєте одну структуру даних досить часто з певним типом даних, ви також можете обернути безпечну для версії версію, яка просто працює з бітами та байтами та покажчиками функцій, і 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 ++.

Простий приклад - інтернування коротких рядків (як це зазвичай буває з рядками, які використовуються для пошукових клавіш - вони, як правило, дуже короткі). Навіщо турбуватися з усіма цими рядками змінної довжини, розміри яких змінюються під час виконання, що передбачає нетривіальну побудову та руйнування (оскільки нам, можливо, доведеться купувати та звільняти)? Як щодо того, щоб просто зберігати ці речі в центральній структурі даних, як-от захищена від потоків трие або хеш-таблиця, призначена саме для інтернування інтерфейсу, а потім зверніться до цих рядків із звичайним старим int32_tабо:

struct IternedString 
{
    int32_t index;
};

... в наших хеш-таблицях, червоно-чорних деревах, пропускних списках тощо, якщо нам не потрібна лексикографічна сортування? Тепер усі наші інші структури даних, які ми кодували для роботи з 32-бітовими цілими числами, тепер можуть зберігати ці інтерновані рядкові ключі, які фактично є лише 32-бітними ints. І я принаймні знайшов у своїх випадках використання (можливо, це просто мій домен, оскільки я працюю в таких областях, як ретракція, обробка сітки, обробка зображень, системи частинок, прив'язка до мов скриптів, низькорівневі реалізації набору багатопотокових GUI тощо) - речі низького рівня, але не такі низькі, як ОС), що код збігом випадково стає більш ефективним і простим, просто зберігаючи індекси на такі речі. Це робить так, що я часто працюю, скажімо, 75% часу, просто int32_tтаfloat32 в моїх нетривіальних структурах даних або просто зберіганні речей однакового розміру (майже завжди 32-розрядні).

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

Тестування та надійність

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

Деякі незначні дублювання коду стають набагато простішими в цих випадках, оскільки дублювання коду - це лише тягар технічного обслуговування, якщо вам доведеться вносити каскадні зміни в дублюваний код. Ви усуваєте одну з найважливіших причин зміни такого зайвого коду, переконуючись, що він надто надійний і дійсно добре підходить для того, що ви не робите.

Моє почуття естетики змінювалося з роками. Мені більше не дратується, тому що я бачу, як одна бібліотека реалізує крапковий продукт або якусь тривіальну логіку SLL, яка вже реалізована в іншій. Мене дратує лише тоді, коли речі погано перевірені і ненадійні, і я виявив, що набагато більш продуктивний спосіб мислення. Я справді мав справу з кодовими базами, які дублювали помилки через дубльований код, і побачив, що найгірші випадки кодування копіювання та вставки, які роблять те, що повинно було бути тривіальним зміною одного центрального місця, перетворюються на багато схильних до помилок каскадної зміни для багатьох. Але багато з тих часів це було наслідком поганого тестування, коду не вдалося стати надійним та хорошим у тому, що він робив. До того, як я працював у баггічних застарілих базах даних, мій розум асоціював усі форми дублювання коду як такі, що мають дуже високу ймовірність дублювання помилок і вимагають каскадних змін. Тим не менш, мініатюрна бібліотека, яка робить одне дуже добре і надійно, знайде дуже мало причин, щоб змінитись у майбутньому, навіть якщо в ній є якийсь надмірний вигляд коду. Мої пріоритети були відключені тоді, коли дублювання більше дратувало мене, ніж низька якість та відсутність тестування. Ці останні речі повинні бути головним пріоритетом.

Копіювання коду для мінімалізму?

Це смішна думка, що вискочила мені в голову, але розглянемо випадок, коли ми можемо зіткнутися з бібліотекою C і C ++, яка робить те саме: обидва мають приблизно однаковий функціонал, однаковий обробка помилок, одна не суттєво ефективніше за інших тощо. І найголовніше, що обидва грамотно реалізовані, добре перевірені та надійні. На жаль, я маю тут говорити гіпотетично, оскільки я ніколи не знаходив нічого близького до ідеального співставлення. Але у найближчих речей, які я коли-небудь знаходив у цьому побічному порівнянні, часто було те, що бібліотека C була набагато набагато меншою, ніж еквівалент C ++ (іноді 1/10 від її розміру коду).

І я вважаю, що причина цього полягає в тому, що, знову ж таки, для вирішення проблеми загальним способом, який обробляє найширший спектр випадків використання замість одного конкретного випадку використання, може знадобитися сотні тисяч рядків коду, тоді як для останнього може знадобитися лише десяток. Незважаючи на надмірність, і незважаючи на те, що стандартна бібліотека С є непридатною, коли йдеться про надання стандартних структур даних, вона часто в кінцевому підсумку створює менше коду в людських руках для вирішення тих же проблем, і я думаю, що це в першу чергу пов'язано з до відмінностей у людських тенденціях між цими двома мовами. Один сприяє вирішенню речей щодо дуже конкретного випадку використання, інший має намір пропонувати більш абстрактні та загальні рішення проти найширшого спектру випадків використання, але кінцевий результат цього не відповідає '

Днями я дивився на чийсь Raytracer на Github, і він був реалізований в C ++ і вимагав так багато коду для іграшкового Raytracer. І я не витратив стільки часу на перегляд коду, але там було судно загальних конструкцій, яке обробляло шлях, набагато більше, ніж те, що потрібно було б для перенапруги. І я визнаю цей стиль кодування, тому що раніше я використовував C ++ таким же чином у вигляді супер способу знизу вгору, зосереджуючись на тому, щоб спершу створити повноцінну бібліотеку дуже загальних цільових структур даних, що йдуть вище та далі проблема, а потім вирішення фактичної проблеми на секунду. Але в той час як ці загальні структури можуть де-небудь усунути надмірність тут і там і матимуть багато разів використовувати в нових контекстах, в обмін вони сильно роздувають проект, обмінюючись трохи надмірності з судновим навантаженням непотрібного коду / функціональності, а останній не обов'язково простіше у підтримці, ніж перший. Навпаки, мені часто важче підтримувати, оскільки важко підтримувати дизайн чогось загального, що повинно чітко збалансувати дизайнерські рішення проти найширшого кола можливих потреб.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.