Следует ли использовать предварительные декларации вместо включений везде, где это возможно? - PullRequest
71 голосов
/ 28 марта 2012

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

//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 ]

56 голосов
/ 28 марта 2012

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

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

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

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

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

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

36 голосов
/ 28 марта 2012

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

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

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

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

С помощью типа Incomplete вы можете:

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

С типом Incomplete вы не можете:

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

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

Удобство.

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

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

11 голосов
/ 28 марта 2012

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

// 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 или более местах, и это нехорошо.

8 голосов
/ 30 декабря 2016

Интересный факт, в своем руководстве по стилю C ++ , Google рекомендует использовать #include везде, но чтобы избежать циклических зависимостей.

8 голосов
/ 23 июня 2014

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

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

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

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

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

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

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

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

Файл 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
5 голосов
/ 30 декабря 2016

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

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

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

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

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

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

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

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

Форвард, объявляющий класс:

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

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

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

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

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

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

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

2 голосов
/ 28 марта 2012

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

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

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

0 голосов
/ 27 июня 2013

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

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

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

</p> <pre><code>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

...