Следует ли использовать прямые объявления вместо includes, где это возможно?
всякий раз, когда объявление класса использует другой класс только в качестве указателей, имеет ли смысл использовать объявление класса вперед вместо включения headerfile, чтобы упреждающе избежать проблем с круговыми зависимостями? так, вместо:
//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"
...
есть ли причина, почему бы не сделать это везде, где это возможно?
9 ответов
метод прямого объявления почти всегда лучше. (Я не могу придумать ситуацию, когда включение файла, в котором вы можете использовать прямое объявление, лучше, но я не собираюсь говорить, что это всегда лучше на всякий случай).
нет никаких недостатков для классов прямого объявления, но я могу подумать о некоторых недостатках для включения заголовков без необходимости:
больше времени компиляции, так как все единицы перевода, в том числе
C.h
также включаютA.h
, хотя им это может не понадобиться.возможно, включая другие заголовки, которые вам не нужны косвенно
загрязнение единицы перевода символами, которые вам не нужны
вам может потребоваться перекомпилировать исходные файлы, которые включают этот заголовок, если он изменится (@PeterWood)
Да, использование прямых объявлений всегда лучше.
некоторые из преимуществ, которые они предоставляют, являются:
- уменьшено время компиляции.
- нет загрязнения пространства имен.
- (в некоторых случаях)может уменьшить размер генерируемых двоичных файлов.
- время перекомпиляции может быть существенно снижена.
- избегая потенциального столкновения имен препроцессоров.
- реализация PIMPL идиома таким образом, обеспечивая средство скрытия реализации от интерфейса.
однако прямое объявление класса делает этот конкретный класс неполного типа и это серьезно ограничивает то, какие операции вы можете выполнять на неполном типе.
Вы не можете выполнять какие-либо операции, для которых компилятор должен знать макет класса.
с неполным типом вы можете:
- объявить член указателем или ссылка на неполный тип.
- объявить функции или методы, которые принимают/возвращают неполные типы.
- определите функции или методы, которые принимают/возвращают указатели/ссылки на неполный тип (но без использования его членов).
с неполным типом вы не можете:
- используйте его как базовый класс.
- используйте его для объявления члена.
- определите функции или методы, использующие этот тип.
есть ли причина, почему бы не сделать это везде, где это возможно?
удобство.
Если вы знаете заранее, что любой пользователь этого файла заголовка обязательно должен будет также включить определение A
делать что угодно (или, возможно, большую часть времени). Тогда удобно просто включить его раз и навсегда.
это довольно щекотливая тема, так как слишком либеральное использование этого правила больших пальцев даст почти неразложимый код. Обратите внимание, что Boost подходит к проблеме по-разному, предоставляя конкретные заголовки "удобства", которые связывают пару близких функций вместе.
один случай, когда вы не хотите иметь прямые объявления, - это когда они сами по себе сложны. Это может произойти, если некоторые из ваших классов шаблонны, как в следующем примере:
// 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>*);
Forward-объявления такие же, как дублирование кода: если код имеет тенденцию сильно меняться, вы должны менять его в 2 местах или более каждый раз, и это не хорошо.
использовать вперед деклараций, а не включает возможности?
нет, явные прямые заявления не следует рассматривать в качестве общего руководства. Форвардные объявления по существу копируют и вставляют или неправильно пишут код, который в случае обнаружения ошибки в нем необходимо исправить везде, где используются форвардные объявления. Это может быть подвержено ошибкам.
чтобы избежать несоответствий между объявлениями" вперед " и его определениями, поместите объявления в файле заголовка и включают этот файл заголовка как в определяющие, так и в исходные файлы с использованием объявления.
в этом частном случае, однако, когда только непрозрачный класс объявлен вперед, это прямое объявление может быть в порядке использования, но в целом "использовать прямые объявления вместо includes когда это возможно", как говорится в названии этого потока, может быть довольно рискованным.
вот несколько примеров "невидимых рисков" в отношении форвардных деклараций (невидимые риски = несоответствия деклараций, не обнаруженные компилятором или компоновщиком):
явные прямые объявления символов, представляющих данные, могут быть небезопасными, поскольку такие прямые объявления могут потребовать правильного знания размера (размера) типа данных.
явные прямые объявления символов, представляющих функции, также могут быть небезопасными, как типы параметров и количество параметры.
пример ниже иллюстрирует это, например, два опасных прямых объявления данных, а также функции:
файл А. 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.с:
#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()
из-за имени c++ искажение.
запуск программы:
> ./a.out
truncated=abcd, forgotten="♀♥♂☺☻"
accessing data[1270][1023]
Segmentation fault
забавный факт, в своем стиле C++ компания Google recommands с помощью #include
всюду, но чтобы избежать циклических зависимостей.
есть ли причина, почему бы не сделать это везде, где это возможно?
абсолютно: она нарушает инкапсуляцию, требуя от пользователя класса или функции знать и дублировать детали реализации. Если эти детали реализации изменяются, код, который объявляет forward, может быть сломан, а код, который полагается на заголовок, будет продолжать работать.
вперед, объявляя функцию:
требуется знать, что он реализован как функция, а не экземпляр статического объекта функтора или (gasp!) макрос
требует дублирования значений по умолчанию для параметров по умолчанию:
требуется знать его фактическое имя и пространство имен, так как это может быть просто
using
объявление, которое тянет его в другое пространство имен, возможно, под псевдонимом, иможет потерять встроенную оптимизацию.
если потребляющий код полагается на заголовок, тогда все эти детали реализации могут быть изменены поставщиком функций без нарушения вашего кода.
вперед объявляя класс:
требуется знать, является ли он производным классом и базовым классом (классами), из которого он получен,
требуется знать, что это класс, а не просто typedef или конкретный экземпляр шаблона класса (или знать, что это шаблон класса и получить все параметры шаблона и значения по умолчанию правильно),
требуются зная истинное имя и пространство имен класса, так как это может быть
using
объявление, которое тянет его в другое пространство имен, возможно, под псевдонимом, итребует знания правильных атрибутов (возможно, у него есть специальные требования к выравниванию).
опять же, прямое объявление нарушает инкапсуляцию этих деталей реализации, делая ваш код более хрупкий.
Если вам нужно сократить зависимости заголовков, чтобы ускорить время компиляции, то попросите поставщика класса/функции/библиотеки предоставить специальный заголовок прямого объявления. Стандартная библиотека делает это с помощью <iosfwd>
. Эта модель сохраняет инкапсуляцию деталей реализации и дает сопровождающему библиотеки возможность изменять эти детали реализации, не нарушая код, при этом уменьшая нагрузку на компилятор.
другой вариант-использовать идиому pimpl, которая скрывает детали реализации еще лучше и ускоряет компиляцию за счет небольших накладных расходов во время выполнения.
есть ли причина, почему бы не сделать это везде, где это возможно?
единственная причина, по которой я думаю, это сохранить некоторые набрав.
без прямых объявлений вы можете включить файл заголовка только один раз, но я не советую делать это на любых довольно больших проектах из-за недостатков, указанных другими людьми.
есть ли причина, почему бы не сделать это везде, где это возможно?
Да - Производительность. Объекты класса хранятся вместе со своими элементами данных в памяти. Когда вы используете указатели, память на фактический объект, на который указывает, хранится в другом месте кучи, обычно далеко. Это означает, что доступ к этому объекту приведет к пропуску кэша и перезагрузке. Это может иметь большое значение в ситуациях, когда производительность имеет решающее значение.
на моем ПК быстрее() функция работает приблизительно 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