Следует ли использовать форвардные декларации, а не включать где это возможно?
Когда декларация класса использует другой класс только как указатели, имеет ли смысл использовать декларацию прямого класса вместо включения заголовочного файла, чтобы упреждающе избегать проблем с круговыми зависимостями? поэтому вместо того, чтобы:
//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