Следует ли использовать предварительные декларации вместо включений везде, где это возможно?
Всякий раз, когда объявление класса использует другой класс только в качестве указателей, имеет ли смысл использовать прямое объявление класса вместо включения файла заголовка, чтобы превентивно избежать проблем с циклическими зависимостями? поэтому вместо того, чтобы:
//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 Idiom, таким образом, предоставляет возможность скрыть реализацию от интерфейса.
Однако объявление класса Forward делает этот конкретный класс типом Incomplete, что строго ограничивает операции, которые вы можете выполнять над типом Incomplete.
Вы не можете выполнять какие-либо операции, которые понадобятся компилятору, чтобы узнать расположение класса.
С Incomplete type вы можете:
- Объявите член указателем или ссылкой на неполный тип.
- Объявите функции или методы, которые принимают / возвращают неполные типы.
- Определите функции или методы, которые принимают / возвращают указатели / ссылки на неполный тип (но без использования его членов).
С Incomplete type вы не можете:
- Используйте это как базовый класс.
- Используйте это, чтобы объявить участника.
- Определите функции или методы, используя этот тип.
Есть ли причина, почему бы не сделать это везде, где это возможно?
Удобство.
Если вы знаете заранее, что любой пользователь этого файла заголовка обязательно должен будет включить определение 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>*);
Форвардные объявления - это то же самое, что и дублирование кода: если код имеет тенденцию к значительным изменениям, вы должны каждый раз менять его в 2 или более местах, и это не хорошо.
Интересный факт: в своем руководстве по стилю C++ Google рекомендует использовать #include
везде, но чтобы избежать круговых зависимостей.
Следует ли использовать предварительные декларации вместо включений везде, где это возможно?
Нет, явные предварительные декларации не должны рассматриваться в качестве общего руководства. Форвардные объявления, по сути, представляют собой скопированный и вставленный код или код с ошибкой, который в случае обнаружения в нем ошибки необходимо исправлять везде, где используются форвардные объявления. Это может быть подвержено ошибкам.
Чтобы избежать несоответствия между "прямыми" объявлениями и их определениями, поместите объявления в файл заголовка и включите этот файл заголовка в исходные файлы определения и использования объявлений.
В этом особом случае, однако, когда только непрозрачный класс объявлен форвардом, это прямое объявление может быть приемлемым для использования, но в целом "использовать форвардные декларации вместо включений везде, где это возможно", как сказано в заголовке этого потока, может быть довольно рискованным.
Вот несколько примеров "невидимых рисков", касающихся форвардных объявлений (невидимые риски = несоответствия объявлений, которые не обнаружены компилятором или компоновщиком):
Явные прямые декларации символов, представляющих данные, могут быть небезопасными, поскольку такие прямые декларации могут потребовать правильного знания размера (размера) типа данных.
Явные предварительные объявления символов, представляющих функции, также могут быть небезопасными, например, типы параметров и количество параметров.
Пример ниже иллюстрирует это, например, два опасных предварительных объявления данных, а также функции:
Файл ac:
#include <iostream>
char data[128][1024];
extern "C" void function(short truncated, const char* forgotten) {
std::cout << "truncated=" << std::hex << truncated
<< ", forgotten=\"" << forgotten << "\"\n";
}
Файл bc:
#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
Есть ли причина, почему бы не сделать это везде, где это возможно?
Абсолютно: он нарушает инкапсуляцию, требуя, чтобы пользователь класса или функции знал и дублировал детали реализации. Если эти детали реализации изменятся, код, который пересылает объявления, может быть поврежден, в то время как код, основанный на заголовке, продолжит работать.
Вперед, объявляя функцию:
требует знания того, что он реализован как функция, а не как экземпляр статического функторного объекта или (задыхаясь!) макроса,
требует дублирования значений по умолчанию для параметров по умолчанию,
требует знания его фактического имени и пространства имен, так как это может быть просто
using
объявление, которое тянет его в другое пространство имен, возможно, под псевдонимом, иможет проиграть на встроенной оптимизации.
Если потребляющий код опирается на заголовок, то все эти детали реализации могут быть изменены поставщиком функций, не нарушая ваш код.
Форвард, объявляющий класс:
требует знания, является ли это производным классом и базовым классом (ами), из которого он получен,
требует знания того, что это класс, а не просто определение типа или конкретное создание шаблона класса (или знание того, что это шаблон класса и получение всех параметров шаблона и значений по умолчанию),
требует знания истинного имени и пространства имен класса, так как это может быть
using
объявление, которое тянет его в другое пространство имен, возможно, под псевдонимом, итребует знания правильных атрибутов (возможно, у него есть особые требования к выравниванию).
Опять же, прямое объявление нарушает инкапсуляцию этих деталей реализации, делая ваш код более хрупким.
Если вам нужно сократить зависимости заголовка, чтобы ускорить время компиляции, тогда обратитесь к провайдеру класса / функции / библиотеки, который предоставит специальный заголовок пересылки объявлений. Стандартная библиотека делает это с <iosfwd>
, Эта модель сохраняет инкапсуляцию деталей реализации и дает организатору библиотеки возможность изменять эти детали реализации, не нарушая ваш код, все это, одновременно снижая нагрузку на компилятор.
Другой вариант - использовать идиому pimpl, которая еще лучше скрывает детали реализации и ускоряет компиляцию за счет небольших накладных расходов во время выполнения.
Есть ли причина, почему бы не сделать это везде, где это возможно?
Единственная причина, по которой я думаю, это сохранить набор текста.
Без предварительных деклараций вы можете включить заголовочный файл только один раз, но я не советую делать это на любых довольно больших проектах из-за недостатков, на которые указывают другие люди.
Есть ли причина, почему бы не сделать это везде, где это возможно?
Да - Производительность. Объекты класса хранятся вместе со своими членами данных в памяти. Когда вы используете указатели, память на фактический объект, на который указывает, хранится где-то в куче, обычно далеко. Это означает, что доступ к этому объекту приведет к потере кеша и перезагрузке. Это может иметь большое значение в ситуациях, когда производительность имеет решающее значение.
На моем ПК функция Faster() работает примерно в 2000 раз быстрее, чем функция 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