Разделение кода класса С++ на несколько файлов, каковы правила?
Время размышления. Почему вы все равно хотите разделить файл?
Как видно из названия, конечная проблема, которую я имею, - это множественные ошибки компоновщика определения. Я действительно исправил проблему, но я не исправил проблему правильно. Перед началом работы я хочу обсудить причины разделения файла класса на несколько файлов. Я попытался использовать все возможные сценарии здесь - если я пропустил, пожалуйста, напомните мне, и я могу внести изменения. Надеемся, что правильнее:
Причина 1 Чтобы сэкономить место:
У вас есть файл, содержащий объявление класса со всеми членами класса. Вы помещаете #include защитников вокруг этого файла (или #pragma один раз), чтобы избежать конфликтов, если вы # включаете файл в два разных файла заголовка, которые затем включаются в исходный файл. Вы компилируете отдельный исходный файл с реализацией любых методов, объявленных в этом классе, поскольку он загружает много строк кода из вашего исходного файла, который немного очищает вещи и вводит некоторый порядок в вашу программу.
Пример. Как вы можете видеть, приведенный ниже пример можно улучшить, разделив реализацию методов класса на другой файл. (Файл .cpp)
// my_class.hpp
#pragma once
class my_class
{
public:
void my_function()
{
// LOTS OF CODE
// CONFUSING TO DEBUG
// LOTS OF CODE
// DISORGANIZED AND DISTRACTING
// LOTS OF CODE
// LOOKS HORRIBLE
// LOTS OF CODE
// VERY MESSY
// LOTS OF CODE
}
// MANY OTHER METHODS
// MEANS VERY LARGE FILE WITH LOTS OF LINES OF CODE
}
Причина 2 Чтобы предотвратить множественные ошибки компоновщика определений:
Возможно, это основная причина, по которой вы разделили бы реализацию от объявления. В приведенном выше примере вы можете переместить тело метода вне класса. Это сделало бы его более чистым и структурированным. Однако в соответствии с этим question приведенный выше пример имеет неявные спецификаторы inline
. Перемещение реализации из класса в класс вне класса, как в приведенном ниже примере, вызовет ошибки компоновщика, поэтому вы должны либо вставить все, либо переместить определения функций в файл .cpp.
Пример: _ Пример ниже приведет к "ошибкам компоновщика нескольких определений", если вы не переместите определение функции в файл .cpp или не укажете функцию как встроенную.
// my_class.hpp
void my_class::my_function()
{
// ERROR! MULTIPLE DEFINITION OF my_class::my_function
// This error only occurs if you #include the file containing this code
// in two or more separate source (compiled, .cpp) files.
}
Чтобы устранить проблему:
//my_class.cpp
void my_class::my_function()
{
// Now in a .cpp file, so no multiple definition error
}
Или:
// my_class.hpp
inline void my_class::my_function()
{
// Specified function as inline, so okay - note: back in header file!
// The very first example has an implicit `inline` specifier
}
Причина 3 Вы хотите снова сохранить место, но на этот раз вы работаете с классом шаблона:
Если мы работаем с классами шаблонов, мы не можем переместить реализацию в исходный файл (файл .cpp). То, что в настоящее время не разрешено (я предполагаю) либо стандартным, ни текущим компилятором. В отличие от первого примера Reason 2, мы можем поместить реализацию в файл заголовка. Согласно этому question, причина в том, что методы класса шаблонов также подразумевали спецификаторы inline
. Это верно? (Кажется, это имеет смысл.) Но никто, похоже, не знал вопроса, на который я только что ссылался!
Итак, два примера ниже идентичны?
// some_header_file.hpp
#pragma once
// template class declaration goes here
class some_class
{
// Some code
};
// Example 1: NO INLINE SPECIFIER
template<typename T>
void some_class::class_method()
{
// Some code
}
// Example 2: INLINE specifier used
template<typename T>
inline void some_class::class_method()
{
// Some code
}
Если у вас есть заголовочный файл класса шаблона, который становится огромным из-за всех функций, которые у вас есть, то я считаю, что вам разрешено переместить определения функций в другой файл заголовка (обычно это .tpp файл?), а затем #include file.tpp
в конце вашего файла заголовка, содержащего объявление класса. Вы не должны включать этот файл нигде, но, следовательно, .tpp
, а не .hpp
.
Я предполагаю, что вы также можете сделать это с помощью встроенных методов обычного класса? Это разрешено также?
Время запроса
Итак, я сделал несколько утверждений, большинство из которых относятся к структурированию исходных файлов. Я думаю, что все, что я сказал, было правильным, потому что я сделал некоторые фундаментальные исследования и "узнал некоторые вещи", но это вопрос, и поэтому я не знаю точно.
Что это сводится к тому, как организовать код внутри файлов. Я думаю, что я выяснил структуру, которая всегда будет работать.
Вот что я придумал. (Это, например, "Стандарт организации/структуры файла кода класса Ed Bird". Не знаю, будет ли он еще очень полезен, это вопрос о спросе.)
- 1: Объявить класс (шаблон или другое) в файле
.hpp
, включая все методы, функции друзей и данные.
- 2: В нижней части файла
.hpp
, #include
a .tpp
файл, содержащий реализацию любых методов inline
. Создайте файл .tpp
и убедитесь, что все методы указаны как inline
.
- 3Все остальные члены (не встроенные функции, функции друзей и статические данные) должны быть определены в файле
.cpp
, который #include
файл .hpp
вверху, чтобы предотвратить ошибки, такие как "класс ABC не был объявлен". Поскольку все в этом файле будет иметь внешнюю связь, программа будет правильно связываться.
Существуют ли такие стандарты в промышленности? Будет ли стандарт я придумал работу во всех случаях?
Ответы
Ответ 1
Ваши три точки звучат правильно. Это стандартный способ делать что-то (хотя я раньше не видел расширения .tpp, обычно это .inl), хотя лично я просто добавлял встроенные функции в нижней части заголовочных файлов, а не в отдельный файл.
Вот как я упорядочиваю свои файлы. Я пропустил файл декларации перед простыми классами.
MyClass-fwd.h
#pragma once
namespace NS
{
class MyClass;
}
myclass.h
#pragma once
#include "headers-needed-by-header"
#include "myclass-fwd.h"
namespace NS
{
class MyClass
{
..
};
}
myclass.cpp
#include "headers-needed-by-source"
#include "myclass.h"
namespace
{
void LocalFunc();
}
NS::MyClass::...
Заменить прагму с защитой заголовка в соответствии с предпочтением.
Причиной такого подхода является сокращение зависимостей заголовков, которые замедляют время компиляции в больших проектах. Если вы не знаете, вы можете переслать объявление классу, который будет использоваться в качестве указателя или ссылки. Полное объявление требуется только при создании, создании или использовании членов класса.
Это означает, что другой класс, который использует класс (принимает параметры по указателю/ссылке), должен включать заголовок fwd в свой собственный заголовок. Полный заголовок затем включается в исходный файл второго класса. Это значительно уменьшает количество ненужного мусора, которое вы получаете, когда тянет в большой заголовок, который тянет в другой большой заголовок, который тянет другой...
Следующий совет - это неназванное пространство имен (иногда называемое анонимным пространством имен). Это может отображаться только в исходном файле, и это похоже на скрытое пространство имен, видимое только этому файлу. Здесь вы можете разместить локальные функции, классы и т.д., Которые используются только исходным файлом. Это предотвращает столкновения имен, если вы создаете что-то с тем же именем в двух разных файлах. (Например, две локальные функции F могут давать ошибки компоновщика).
Ответ 2
Основная причина ветки интерфейса от реализации состоит в том, что вам не нужно перекомпилировать весь код, когда что-то в реализации изменяется; вам нужно только перекомпилировать исходные файлы, которые изменились.
Что касается "Объявить класс (шаблон или иное)", template
не является class
. template
- это шаблон для создания классов. Более важно, тем не менее, вы определяете класс или шаблон в заголовке. Определение класса включает в себя объявления его функций-членов, а неинные функции-члены определены в одном или нескольких исходных файлах. Встроенные функции-члены и все шаблонные функции должны быть определены в заголовке независимо от того, какую комбинацию прямых определений и директив #include
вы предпочитаете.
Ответ 3
Существуют ли такие стандарты в промышленности?
Да. И снова стандарты кодирования, которые сильно отличаются от тех, которые вы указали, также можно найти в промышленности. В конце концов, вы говорите о стандартах кодирования, а стандарты кодирования варьируются от хорошего до плохого до уродливого.
Будет ли стандарт работать со мной во всех случаях?
Абсолютно нет. Например,
template <typename T> class Foo {
public:
void some_method (T& arg);
...
};
Здесь определение шаблона класса Foo
не знает ничего об этом параметре шаблона T. Что делать, если для некоторого шаблона класса определения методов варьируются в зависимости от параметров шаблона? Ваше правило № 2 здесь просто не работает.
Другой пример: что, если соответствующий исходный файл огромен, тысяча строк длинная или длинная? Иногда имеет смысл обеспечить реализацию в нескольких исходных файлах. Некоторые стандарты ограничивают диктовку одной функции на файл (личное мнение: Yech!).
В другой крайности файла с длиною в тысячу с лишним строк - это класс, у которого нет исходных файлов. Вся реализация находится в заголовке. Там многое можно сказать о реализации только для заголовков. Если ничего другого, это упрощает, иногда значительно, проблему связывания.