Чи слід використовувати передові декларації замість включень, де це можливо?


78

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

//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"
...

Чи є якась причина, чому не робити цього скрізь, де це можливо?


5
ммм - це відповідь на запитання зверху чи знизу?
Mat

1
ваше справжнє запитання (внизу) - AFAIK не має жодної причини не використовувати в цьому випадку пряму декларацію ...
Нім

13
Це трохи залежить від того, що ви маєте на увазі під словом "використовує інший клас лише як вказівники". Існує неприємний випадок, коли ви можете deleteвказувати, використовуючи лише переадресоване оголошення, але якщо клас насправді має нетривіальний деструктор, тоді ви отримуєте UB. Отже, якщо delete"використовує лише вказівники", тоді так, є причина. Якщо це не враховує, не так вже й багато.
Steve Jessop

Хіба кругова залежність все ще не існує і просто прихована від компілятора? Якщо так, обидві тактики, що завжди включають і завжди роблять декларацію вперед, не вчать вас, як уникнути кругових залежностей. Однак я повинен визнати, що за допомогою декларацій, що надсилаються, їх може бути простіше знайти.
grenix

1
Якщо клас насправді визначається як структура, деякі компілятори можуть скаржитися. Якщо клас А є похідним класом, у вас є проблеми. Якщо клас A визначено в іншому просторі імен, а заголовок просто втягує його в цей за допомогою декларації за допомогою, у вас можуть виникнути проблеми. Якщо клас A насправді є псевдонімом (або макросом!), У вас є проблеми. Якщо клас A насправді є typedef, у вас є проблеми. Якщо клас A насправді є шаблоном класу з параметрами шаблону за замовчуванням, у вас виникли проблеми. Так, є причина не пересилати заяву: це порушує інкапсуляцію деталей реалізації.
Адріан Маккарті

Відповіді:


60

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

У класів, що оголошують вперед, немає мінусів, але я можу подумати про деякі мінуси, якщо включати заголовки без потреби:

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

  • можливо, включаючи інші заголовки, які вам не потрібні побічно

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

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


11
Крім того, підвищена ймовірність перекомпіляції.
Пітер Вуд

9
"Я не можу подумати про ситуацію, коли включення файлу, де ви можете використовувати переадресацію декларації, є кращим" - коли переадресація дасть UB, див. Мій коментар до головного питання. Ви маєте рацію бути обережними, я думаю :-)
Стів Джессоп,

1
@Luchian: Оскільки відповідь це чи ні, залежить те, що спочатку мав на увазі запитувач, я не хочу розміщувати "відповіді", які зводили б до дрібниць. Можливо, запитувач ніколи не мріяв би написати deleteзаяву C.h.
Steve Jessop

2
Недоліком є ​​більше роботи та більше коду! І ще крихкість. Не можна сказати, що немає мінусів.
usr

2
Що робити, якщо є 5 класів? Що робити, якщо пізніше потрібно буде додати трохи? Ви просто зосередилися на найкращому варіанті для своєї точки зору.
usr

37

Так, використання прямих декларацій завжди краще.

Деякі переваги, які вони надають:

  • Скорочений час складання.
  • Не забруднює простір імен.
  • (У деяких випадках) може зменшити розмір ваших сформованих двійкових файлів.
  • Час рекомпіляції можна значно скоротити.
  • Уникнення потенційного зіткнення назв препроцесора.
  • Реалізація PIMPL Idiom, таким чином, забезпечує приховування реалізації від інтерфейсу.

Однак попереднє оголошення класу робить цей клас незавершеним типом, що суворо обмежує операції, які можна виконувати над типом незавершеного.
Ви не можете виконувати жодних операцій, які потребували б компілятора, щоб знати макет класу.

За допомогою неповного типу ви можете:

  • Оголосіть учасника вказівником або посиланням на неповний тип.
  • Оголосіть функції або методи, які приймають / повертають неповні типи.
  • Визначте функції або методи, які приймають / повертають покажчики / посилання на неповний тип (але без використання його членів).

При неповному типі ви не можете:

  • Використовуйте його як базовий клас.
  • Використовуйте його для оголошення учасника.
  • Визначте функції або методи, використовуючи цей тип.

1
"Однак попереднє оголошення класу робить цей клас незавершеним типом, що суворо обмежує операції, які ви можете виконувати над типом незавершеного." Ну так, але якщо ви можете переадресувати це, це означає, що вам не потрібен повний тип у заголовку. І якщо вам потрібен повний тип у файлі, який містить цей заголовок, просто додайте заголовок для потрібного вам типу. IMO, це перевага - це змушує вас включати все, що вам потрібно, до вашого файлу реалізації, а не покладатися на те, що воно буде включене деінде.
Luchian Grigore

Скажімо, хтось змінює цей заголовок і замінює включення на переадресацію. Тоді вам доведеться піти і змінити всі файли, що містять цей заголовок, використовувати відсутній тип, але не включати самі заголовок відсутнього типу (хоча вони повинні).
Luchian Grigore

1
@LuchianGrigore: .. але якщо ви можете переслати заяву про це ... , вам доведеться спробувати це перевірити. Отже, немає фіксованого правила, щоб просто переходити до Форвардних декларацій і не включати заголовки, знаючи, що правила допомагають організувати ваші реалізації .Найпоширенішим використанням оголошень Forward є розбиття кругових залежностей, і саме там вас зазвичай кусає те, чого не можна робити з неповними типами. Кожен вихідний файл і файл заголовка повинен містити всі заголовки, необхідні для компіляції, тому другий аргумент не apply, для початку це просто погано організований код.
Алок Зберегти

1
для PIMPL також може мати сенс використовувати ці переадресаційні декларації лише у приватних частинах вашого класу
grenix

21

Чи є якась причина, чому не робити цього скрізь, де це можливо?

Зручність.

Якщо ви знаєте напередодні фази, що будь-якому користувачеві цього файлу заголовка обов’язково потрібно буде також включити визначення Aробити що-небудь (або, можливо, більшість випадків). Тоді його зручно просто включити раз і назавжди.

Це досить дотепна тема, оскільки занадто ліберальне використання цього правила великих пальців дасть майже некомпільований код. Зауважте, що Boost підходить до проблеми по-різному, надаючи конкретні заголовки «зручності», які об’єднують пару тісних функціональних можливостей.


5
Це єдина відповідь, яка вказує на те, що це робить витрати на продуктивність. +1
usr

З точки зору користувача. Якщо ви переадресуєте все, це означає, що користувач не може просто включити цей файл і негайно розпочати роботу. Вони повинні з’ясувати, які є залежності (можливо, через скаржник компілятора на неповні типи), а також включити ці файли, перш ніж вони зможуть почати використовувати ваші класи. Альтернативою є створення файлу "shared.hpp" або чогось іншого для вашої бібліотеки, де всі заголовки знаходяться у цьому файлі (наприклад, boost, згаданий вище). Вони можуть легко включити його, не з’ясувавши, чому вони не можуть просто «включити і піти».
Тодд,

11

Один із випадків, коли ви не хочете мати переадресаційні декларації, це коли вони самі хитрі. Це може статися, якщо деякі з ваших класів є шаблонами, як у наступному прикладі:

// 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 місцях і більше, і це не корисно.


2
+1 за руйнування консенсусу щодо того, що пряма декларація строго завжди краща :-) IIRC така сама проблема виникає з типами, які є "таємно" екземплярами шаблону, через typedefs. namespace std { class string; }було б неправильно, навіть якби було дозволено розміщувати оголошення класу в просторі імен std, тому що (я думаю) ви не можете законно оголосити вперед декларування typedef так, ніби це клас.
Steve Jessop


8

Чи слід використовувати передові декларації замість включень, де це можливо?

Ні, явні прямі декларації не повинні розглядатися як загальне керівництво. Переадресаційні декларації - це, по суті, копія та вставка, або неправильно написаний код, який у разі виявлення помилки в ньому потрібно виправити всюди, де використовуються переадресовані оголошення. Це може спричинити помилки.

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

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

Ось кілька прикладів "невидимих ​​ризиків" щодо прямих оголошень (невидимі ризики = невідповідність оголошень, які не виявляються компілятором або компонувальником):

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

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

Наведений нижче приклад ілюструє це, наприклад, дві небезпечні декларації вперед даних, а також функції:

Файл змінного струму:

#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

Чи є якась причина, чому не робити цього скрізь, де це можливо?

Абсолютно: він порушує інкапсуляцію, вимагаючи від користувача класу або функції знати та продублювати деталі реалізації. Якщо ці деталі реалізації змінюються, код, який оголошує переадресацію, може бути зламаний, тоді як код, який спирається на заголовок, буде продовжувати працювати.

Попереднє оголошення функції:

  • вимагає знання того, що він реалізований як функція, а не екземпляр об’єкта статичного функтора або (задихається!) макросу,

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

  • вимагає знання його фактичного імені та простору імен, оскільки це може бути просто usingдекларація, яка перетягує його в інший простір імен, можливо, під псевдонімом, і

  • може втратити на вбудованій оптимізації.

Якщо споживаючий код покладається на заголовок, тоді всі ці деталі реалізації можуть бути змінені постачальником функцій, не порушуючи ваш код.

Попереднє оголошення класу:

  • вимагає знання, чи це похідний клас, і базовий клас (и), з якого він походить,

  • потрібно знати, що це клас, а не просто typedef або конкретна інстанція шаблону класу (або знати, що це шаблон класу, і отримувати всі параметри шаблону та значення за замовчуванням правильними),

  • вимагає знання справжнього імені та простору імен класу, оскільки це може бути usingоголошення, яке перетягує його в інший простір імен, можливо, під псевдонімом, і

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

Знову ж таки, декларування вперед порушує інкапсуляцію цих деталей реалізації, роблячи ваш код більш крихким.

Якщо вам потрібно скоротити залежності заголовка, щоб прискорити час компіляції, зверніться до постачальника класу / функції / бібліотеки для надання спеціального заголовка прямих оголошень. Стандартна бібліотека робить це за допомогою <iosfwd>. Ця модель зберігає інкапсуляцію деталей реалізації та надає підтримку бібліотеки можливість змінювати ці деталі реалізації, не порушуючи ваш код, все одночасно зменшуючи навантаження на компілятор.

Інший варіант - використовувати ідіому pimpl, яка ще краще приховує деталі реалізації та пришвидшує компіляцію за рахунок невеликих накладних витрат.


Наприкінці ви рекомендуєте використовувати ідіому pimpl, але вся корисність цієї ідіоми базується на прямих деклараціях. Форвардні декларації та ідіома pimpl - це майже одне і те ж.
user2445507

@ user2445507: Питання було "коли це можливо". Моя думка полягає в тому, що зазвичай не є гарною ідеєю робити «по можливості». Як я вже сказав у своєму передостанньому параграфі, власник інтерфейсу може надавати декларації пересилання, оскільки вони можуть синхронізувати декларації пересилання з фактичним інтерфейсом. З ідіомою pimpl той самий програміст відповідає за пряме оголошення та реалізацію об’єкта impl, тому це цілком нормально.
Адріан Маккарті,

2

Чи є якась причина, чому не робити цього скрізь, де це можливо?

Єдина причина, про яку я думаю, - це зберегти певний текст.

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


1
@Luchian Grigore: можливо, це нормально для якоїсь простої тестової програми
ks1322,

-1

Чи є якась причина, чому не робити цього скрізь, де це можливо?

Так - продуктивність. Об'єкти класу зберігаються разом із членами даних разом у пам'яті. Коли ви використовуєте покажчики, пам'ять до фактичного об'єкта, на який вказано, зберігається в іншому місці купи, зазвичай далеко. Це означає, що доступ до цього об’єкта призведе до пропуску та перезавантаження кешу. Це може сильно змінити ситуації, коли продуктивність має вирішальне значення.

На моєму ПК функція 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


9
Ви відповідаєте на інше запитання ("Чи слід використовувати вказівники?"), А не на запитання ("Коли я використовую лише вказівники, чи є якісь причини не використовувати переадресаційні декларації?").
ніхто

@AndrewMedico, я думаю, що це хороша відповідь, вказуючи на недолік продуктивності, який відповів на питання. "переадресація == за допомогою вказівника"
Ерік,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.