Должен ли я поместить много функций в один файл? Или, более или менее, одна функция на файл?

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

Причины:

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

  2. Если это один класс или одна не-функция-член на файл заголовка, тогда я не буду включать весь беспорядок, когда яinclude заголовочный файл.

  3. Если я внесу небольшое изменение в функцию, то будет перекомпилирована только эта функция.

Однако разбиение всего на несколько заголовков и множество файлов реализации может существенно замедлить компиляцию. В моем проекте большинство функций получают доступ к определенному числу шаблонных других библиотечных функций. Таким образом, этот код будет компилироваться снова и снова, один раз для каждого файла реализации. Компиляция всего моего проекта в настоящее время занимает около 45 минут на одной машине. Существует около 50 объектных файлов, и каждый из них использует одни и те же дорогие для компиляции заголовки.

Может быть, допустимо иметь один класс (или функцию, не являющуюся членом) на файл заголовка, но помещая реализации многих или всех этих функций в один файл реализации, как в следующем примере?

// foo.h
void foo(int n);

// bar.h
void bar(double d);

// foobar.cpp
#include <vector>
void foo(int n) { std::vector<int> v; ... }
void bar(double d) { std::vector<int> w; ... }

Опять же, преимущество заключается в том, что я могу включить только функцию foo или просто функцию bar, и компиляция всего проекта будет быстрее, потому что foobar.cpp один файл, поэтому std::vector<int> (это просто пример здесь для некоторой другой дорогой для компиляции шаблонной конструкции) должен быть скомпилирован только один раз, в отличие от двух, если я скомпилировал foo.cpp а также bar.cpp по отдельности. Конечно, моя причина (3) выше недопустима для этого сценария: после простого изменения foo(){...} я должен перекомпилировать весь, потенциально большой, файл foobar.cpp,

Мне любопытно, каково ваше мнение!

9 ответов

Решение

ИМХО, вы должны объединять элементы в логические группировки и создавать свои файлы на основе этого.

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

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

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

По моему мнению, одна функция на файл может стать беспорядочной. Представьте, что заголовки POSIX и ANSI C были сделаны одинаково.

#include <strlen.h>
#include <strcpy.h>
#include <strncpy.h>
#include <strchr.h>
#include <strstr.h>
#include <malloc.h>
#include <calloc.h>
#include <free.h>
#include <printf.h>
#include <fprintf.h>
#include <vpritnf.h>
#include <snprintf.h>

Один класс на файл - хорошая идея.

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

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

// getShapeColor.h
Color getShapeColor (Shape);

// getTextColor.h
Color getTextColor (Text);

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

При этом даже в случае стандартной библиотеки есть некоторые потенциальные преимущества в разделении отдельных функций. Во-первых, компиляторы могут генерировать полезное предупреждение при использовании небезопасных версий функций, например. strcpy против strncpy, аналогично тому, как g++ использовал для предупреждения о включении vs .

Еще одно преимущество заключается в том, что я больше не буду зависать от включения памяти, когда захочу использовать memmove!

Одна функция на файл имеет техническое преимущество, если вы создаете статическую библиотеку (что, я думаю, является одной из причин, по которой такие проекты, как проект Musl-libc, следуют этому шаблону).

Статические библиотеки связаны с гранулярностью объектного файла, поэтому, если у вас есть статическая библиотека libfoobar.a состоит из *:

 foo.o
     foo1
     foo2
 bar.o
     bar

тогда, если вы связываете библиотеку для bar функция, bar.o участник архива будет связан, но не foo.o член. Если вы ссылаетесь на foo1тогда foo.o член будет связан, привнося, возможно, ненужный foo2 функция.

Возможно, есть и другие способы предотвратить связывание ненужных функций в (-ffunction-sections -fdata-sections а также --gc-sections) но одна функция на файл, вероятно, наиболее надежна.


  • Я игнорирую искажение имени C++ здесь ради простоты

Я вижу некоторые преимущества вашего подхода, но есть несколько недостатков.

1) Включая посылку это кошмар. Вы можете получить 10-20 включений, чтобы получить нужные вам функции. Например, изображение, если STDIO или StdLib был реализован таким образом.

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

3) сделать обслуживание файлов - это боль.

4) Я большой поклонник небольших функций и рефакторинга. Когда вы добавляете накладные расходы (создаете новый файл, добавляете его в систему управления исходным кодом,...), это побуждает людей писать более длинные функции, где вместо того, чтобы разбивать 1 функцию на 3 части, вы просто делаете 1 большую.

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

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

Один мой старый профессор по программированию предложил разбивать модули на каждые несколько сотен строк кода для удобства обслуживания. Я больше не занимаюсь разработкой на C++, но в C# я ограничиваю себя одним классом на файл, и размер файла не имеет значения, если с моим объектом ничего не связано. Вы можете использовать области #pragma, чтобы изящно уменьшить пространство редактора, не зная, есть ли у компилятора C++, но если он есть, то обязательно используйте их.

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

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

Также никогда не повредит иметь несколько исходных файлов, которые определяют одну сущность. Т.е.: 'ServiceConnection.cpp', 'ServiceSettings.cpp' и так далее, и тому подобное.

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

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

Я также пытался разделить файлы в функции на файл, но у него были некоторые недостатки. Иногда функции имеют тенденцию становиться больше, чем нужно (вы не хотите добавлять новый файл c каждый раз), если вы не усердны в рефакторинге своего кода (я не так). В настоящее время я помещаю от 1 до 3 функций в файл c и группирую все файлы c для функциональности в каталоге. Для заголовочных файлов у меня есть Funcionality.h а также Subfunctionality.h так что я могу включить все функции одновременно, когда это необходимо, или просто небольшую служебную функцию, если весь пакет не нужен.

Для части HEADER вы должны объединить элементы в логические группы и создать файлы HEADER на их основе. Это кажется и очень логичным ИМХО.

Для части SOURCE вы должны поместить каждую реализацию функции в отдельный файл SOURCE. (статические функции в данном случае являются исключениями). Поначалу это может показаться нелогичным, но помните, что компилятор знает о функциях, но компоновщик знает только о файлах o/obj и их экспортированных символах. Это может значительно изменить размер выходного файла, и это очень важная проблема для встроенных систем.

Оформить заказ glibc или Visual C++ CRT исходного дерева...

Другие вопросы по тегам