Подтвердить что ты не робот

Разделение кода класса С++ на несколько файлов, каковы правила?

Время размышления. Почему вы все равно хотите разделить файл?

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

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

Существуют ли такие стандарты в промышленности? Будет ли стандарт я придумал работу во всех случаях?

4b9b3361

Ответ 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!).

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