Следует ли использовать форвардные декларации, а не включать где это возможно?

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

//file C.h
#include "A.h"
#include "B.h"

class C{
    A* a;
    B b;
    ...
};

сделайте это вместо:

//file C.h
#include "B.h"

class A;

class C{
    A* a;
    B b;
    ...
};


//file C.cpp
#include "C.h"
#include "A.h"
...

Есть ли причина, почему бы не делать это, когда это возможно?

Ответ 1

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

Нет недостатков для переадресации классов, но я могу подумать о некоторых недостатках для включения заголовков без необходимости:

  • больше времени компиляции, поскольку все единицы перевода, включая C.h, также будут включать A.h, хотя они могут и не нуждаться в нем.

  • возможно, включая другие заголовки, которые вам не нужны косвенно

  • загрязнение единицы перевода символами, которые вам не нужны

  • вам может потребоваться перекомпилировать исходные файлы, которые включают этот заголовок, если он изменится (@PeterWood)

Ответ 2

Да, использование передовых объявлений всегда лучше.

Некоторые из преимуществ, которые они предоставляют:

  • Сокращение времени компиляции.
  • Нет пространства имен.
  • (В некоторых случаях) может уменьшить размер сгенерированных двоичных файлов.
  • Время перекомпиляции может быть значительно уменьшено.
  • Предотвращение потенциального столкновения имен препроцессоров.
  • Реализация ИММОМ PIMPL, предоставляя средства для скрытия реализации из интерфейса.

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

С незавершенным типом вы можете:

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

С незавершенным типом вы не можете:

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

Ответ 3

Есть ли причина, почему бы не делать это, когда это возможно?

Convenience.

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

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

Ответ 4

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

// Forward declarations
template <typename A> class Frobnicator;
template <typename A, typename B, typename C = Frobnicator<A> > class Gibberer;

// Alternative: more clear to the reader; more stable code
#include "Gibberer.h"

// Declare a function that does something with a pointer
int do_stuff(Gibberer<int, float>*);

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

Ответ 5

Следует ли использовать форвардные объявления, а не включать туда, где это возможно?

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

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

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

Вот несколько примеров "невидимых рисков" в отношении форвардных деклараций (невидимые риски = несоответствия декларации, которые не обнаружены компилятором или компоновщиком):

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

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

Приведенный ниже пример иллюстрирует это, например, два опасных форвардных объявления данных, а также функции:

Файл a.c:

#include <iostream>
char data[128][1024];
extern "C" void function(short truncated, const char* forgotten) {
  std::cout << "truncated=" << std::hex << truncated
            << ", forgotten=\"" << forgotten << "\"\n";
}

Файл b.c:

#include <iostream>
extern char data[1280][1024];           // 1st dimension one decade too large
extern "C" void function(int tooLarge); // Wrong 1st type, omitted 2nd param

int main() {
  function(0x1234abcd);                         // In worst case: - No crash!
  std::cout << "accessing data[1270][1023]\n";
  return (int) data[1270][1023];                // In best case:  - Boom !!!!
}

Компиляция программы с помощью g++ 4.7.1:

> g++ -Wall -pedantic -ansi a.c b.c

Примечание. Невидимая опасность, так как g++ не дает ошибок/предупреждений компилятора или компоновщика
Примечание. Опускание extern "C" приводит к ошибке привязки для function() из-за изменения имени С++.

Запуск программы:

> ./a.out
truncated=abcd, forgotten="♀♥♂☺☻"
accessing data[1270][1023]
Segmentation fault

Ответ 6

Забавный факт, в своем языке С++ styleleguide, Google рекомендует использовать #include везде, но избегать круговых зависимостей.

Ответ 7

Есть ли причина, почему бы не делать это, когда это возможно?

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

Переслать декларацию функции:

  • требует знать, что он реализован как функция, а не экземпляр объекта статического функтора или (гаснет!) макрос,

  • требует дублирования значений по умолчанию для параметров по умолчанию,

  • требует знания своего фактического имени и пространства имен, поскольку это может быть просто объявление using, которое вытаскивает его в другое пространство имен, возможно, под псевдонимом и

  • может потерять встроенную оптимизацию.

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

Переслать объявление класса:

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

  • требует знать, что это класс, а не только typedef или конкретный экземпляр шаблона класса (или, зная, что это шаблон класса, и правильно все параметры шаблона и значения по умолчанию),

  • требует знания истинного имени и пространства имен класса, поскольку это может быть объявление using, которое вытаскивает его в другое пространство имен, возможно, под псевдонимом и

  • требует знания правильных атрибутов (возможно, у него есть особые требования к выравниванию).

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

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

Другим вариантом является использование идиомы pimpl, которая еще лучше скрывает детали реализации и ускоряет компиляцию за счет небольших затрат времени выполнения.

Ответ 8

Есть ли причина, почему бы не делать это, когда это возможно?

Единственная причина, по которой я думаю, - сохранить некоторую типизацию.

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

Ответ 9

Есть ли причина, почему бы не делать это, когда это возможно?

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

На моем ПК функция Faster() работает примерно на 2000x быстрее, чем функция Slower():

class SomeClass
{
public:
    void DoSomething()
    {
        val++;
    }
private:
    int val;
};

class UsesPointers
{
public:
    UsesPointers() {a = new SomeClass;}
    ~UsesPointers() {delete a; a = 0;}
    SomeClass * a;
};

class NonPointers
{
public:
    SomeClass a;
};

#define ARRAY_SIZE 100000
void Slower()
{
    UsesPointers list[ARRAY_SIZE];
    for (int i = 0; i < ARRAY_SIZE; i++)
    {
        list[i].a->DoSomething();
    }
}

void Faster()
{
    NonPointers list[ARRAY_SIZE];
    for (int i = 0; i < ARRAY_SIZE; i++)
    {
        list[i].a.DoSomething();
    }
}

код >

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

Это хорошая презентация по этому вопросу и другим показателям эффективности: http://research.scee.net/files/presentations/gcapaustralia09/Pitfalls_of_Object_Oriented_Programming_GCAP_09.pdf