Должен ли я поместить много функций в один файл? Или, более или менее, одна функция на файл? - PullRequest
9 голосов
/ 10 февраля 2009

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

Причины:

  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 ]

9 голосов
/ 10 февраля 2009

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

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

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

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

8 голосов
/ 10 февраля 2009

По моему мнению, одна функция на файл может запутаться. Представьте, что заголовки 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>

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

4 голосов
/ 10 февраля 2009

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

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

// getShapeColor.h
Color getShapeColor (Shape);

// getTextColor.h
Color getTextColor (Text);

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

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

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

1 голос
/ 22 октября 2016

Одна функция на файл имеет техническое преимущество, если вы создаете статическую библиотеку (что, я думаю, является одной из причин, по которой такие проекты, как 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 февраля 2009

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

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

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

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

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

1 голос
/ 10 февраля 2009

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

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

Извлечь исходное дерево CRT для glibc или Visual C ++ ...

1 голос
/ 10 февраля 2009

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

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

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

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

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

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

1 голос
/ 10 февраля 2009

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

1 голос
/ 10 февраля 2009

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

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

...