Как лучше всего перейти от беспорядка шаблонов к чистой архитектуре классов (C++)?

StackOverflow https://stackoverflow.com/questions/324043

  •  11-07-2019
  •  | 
  •  

Вопрос

Предположим, что у вас большая библиотека шаблонов, содержащая около 100 файлов, содержащих около 100 шаблонов, и в общей сложности более 200 000 строк кода.Некоторые шаблоны используют множественное наследование, чтобы сделать использование самой библиотеки довольно простым (т.наследуются от некоторых базовых шаблонов и требуют лишь реализации определенных бизнес-правил).

Все, что существует (выращено за несколько лет), «работает» и используется для проектов.

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

Теперь мне бы очень хотелось упростить архитектуру, чтобы использовать меньше шаблонов и более специализированные классы меньшего размера.

Есть ли какой-нибудь проверенный способ решить эту задачу?С чего лучше начать?

Это было полезно?

Решение

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

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

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

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

Конечно, добавленное время компиляции может быть веской причиной, чтобы в некоторых случаях избегать шаблонов, и если у вас есть только несколько программистов на Java, которые привыкли думать в " традиционном " ; Условия ООП, шаблоны могут их запутать, что может быть еще одной веской причиной, по которой следует избегать шаблонов.

Но с точки зрения архитектуры я считаю, что отказ от шаблонов - это шаг в неверном направлении.

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

Другие советы

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

Кроме этого, " разделяй и властвуй "

Пишите модульные тесты.

Где новый код должен делать то же самое, что и старый код.

По крайней мере, это один совет.

Редактировать:

Если вы установите старый код, который вы заменили новой функциональностью, вы можете немного поставить на новый код.

Проблема в том, что шаблонное мышление сильно отличается от объектно-ориентированного наследования. Трудно ответить на что-либо еще, кроме & Quot; перепроектировать все это и начать с нуля & Quot;.

Конечно, может быть простой способ для конкретного случая. Мы не можем сказать, не зная больше о том, что у вас есть.

Тот факт, что шаблонное решение так сложно поддерживать, в любом случае свидетельствует о плохом дизайне.

Некоторые моменты (но обратите внимание: это действительно не зло. Если вы хотите перейти на не шаблонный код, это может помочь):

<Ч>

Поиск ваших статических интерфейсов . Где шаблоны зависят от того, какие функции существуют? Где им нужны typedefs?

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

Поиск целых списков . Если вы обнаружите, что ваш код использует целочисленные списки, такие как list<1, 3, 3, 1, 3>, вы можете заменить их на std::vector, если все коды, использующие их, могут работать со значениями времени выполнения вместо константных выражений.

Характеристики типа поиска . Существует много кода, проверяющего, существует ли какой-либо typedef или какой-либо метод существует в типичном шаблонном коде. Абстрактные базовые классы решают эти две проблемы, используя чисто виртуальные методы и наследуя typedefs для базы. Часто typedefs нужны только для запуска отвратительных функций, таких как SFINAE , которые также были бы лишними.

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

Объекты функции поиска . Если вы обнаружите, что в вашем коде используются функциональные объекты, вы можете изменить их на использование абстрактных базовых классов, и у вас есть что-то вроде void run(); для их вызова (или, если вы хотите продолжать использовать operator(), лучше! Это также может быть виртуальным). ).

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

Во-первых, не пытайтесь «исправить» все сразу.

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

В-третьих, сначала рассмотрите более дешевые исправления:напримерболее быстрые машины или инструменты распределенной сборки.Как минимум, кинуть всю оперативную память, которую возьмут платы, и выкинуть старые диски.Это действительно имеет значение.Один диск для ОС, один диск для сборки — это дешевый мужской RAID.

Хорошо ли документирована библиотека?Это ваш лучший шанс сделать это. Изучите такие инструменты, как doxygen, которые помогут вам создать такую ​​документацию.

Все учтено?Хорошо, теперь несколько предложений по времени сборки;)


Понимание C++ построить модель:каждый .cpp компилируется индивидуально.Это означает, что множество файлов .cpp со множеством заголовков = огромная сборка.Однако это НЕ совет помещать все в один файл .cpp!Однако есть один трюк (!), который может значительно ускорить сборку: создать один файл .cpp, включающий в себя несколько файлов .cpp, и передать компилятору только этот «главный» файл.Однако вы не можете делать это вслепую — вам нужно понимать, какие типы ошибок это может привести.

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

Использовать предварительно скомпилированные заголовки.(лучше масштабируется на быстрых машинах, см. выше)

Проверьте политику включения заголовков.Хотя каждый файл должен быть «независимым» (т.е.включать все, что должно быть включено кем-то другим), не включайте обильно.К сожалению, я еще не нашел инструмента для поиска ненужных операторов #incldue, но это может помочь потратить некоторое время на удаление неиспользуемых заголовков в файлах «горячих точек».

Создание и использование форвардных объявлений для шаблонов, которые вы используете.Часто вы можете включать заголовок с объявлениями пересылки во многих местах и ​​использовать полный заголовок только в нескольких конкретных местах.Это может значительно улучшить время компиляции.Проверить <iosfwd> заголовок, как стандартная библиотека делает это для потоков ввода-вывода.

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

// .h
template <typename FLOAT> // float or double only
FLOAT CalcIt(int len, FLOAT * values) { ... }

Вы можете объявить перегрузки в заголовке и переместить шаблон в тело:

// .h
float CalcIt(int len, float * values);
double CalcIt(int len, double * values);

// .cpp
template <typename FLOAT> // float or double only
FLOAT CalcItT(int len, FLOAT * values) { ... }

float CalcIt(int len, float * values) { return CalcItT(len, values); }
double CalcIt(int len, double * values) { return CalcItT(len, values); }

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

Проверить, если тот Идиома PIMPL может перемещать код из заголовков в файлы .cpp.

За этим скрывается общее правило: отделите интерфейс вашей библиотеки от реализации.Используйте комментарии, detail пространства имен и отдельные .impl.h заголовки, позволяющие мысленно и физически изолировать то, что должно быть известно извне, от того, как это достигается.Это раскрывает реальную ценность вашей библиотеки (действительно ли она отражает сложность?) и дает вам возможность сначала заменить «легкие цели».


Более конкретный совет - и насколько он полезен - во многом зависит от конкретной библиотеки.

Удачи!

Как уже упоминалось, модульные тесты - хорошая идея. Действительно, вместо того, чтобы ломать ваш код, вводя & Quot; simple & Quot; изменения, которые, скорее всего, проявятся, просто сконцентрируйтесь на создании набора тестов и устранении несоответствия тестам. Выполняйте действия по обновлению тестов при обнаружении ошибок.

Кроме того, я бы предложил обновить ваши инструменты, если это возможно, для устранения проблем, связанных с шаблонами.

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top