Коли я можу використовувати пряму декларацію?


602

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

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


14
Я відчайдушно хочу, щоб це було перейменовано на "коли я повинен ", а відповіді
оновилися

12
@deworde Коли ви говорите, коли "слід", ви запитуєте думку.
AturSams

@deworde, наскільки я розумію, ви хочете використовувати декларації вперед, коли це можливо, щоб покращити час створення та уникнути кругових посилань. Єдиним винятком, про який я можу придумати, є те, коли файл include містить typedefs, і в цьому випадку відбувається компроміс між повторним визначенням typedef (і ризиком його зміни) і включенням цілого файлу (разом з його рекурсивним включенням).
Охад Шнайдер

@OhadSchneider З практичної точки зору я не є великим шанувальником заголовків, які мої. ÷
deworde

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

Відповіді:


962

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

Припускаючи наступну форвардну декларацію.

class X;

Ось що можна, а що не можна робити.

Що можна зробити з неповним типом:

  • Оголосити члена вказівником або посиланням на неповний тип:

    class Foo {
        X *p;
        X &r;
    };
  • Заявити функції або методи, які приймають / повертають неповні типи:

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

    void f3(X*, X&) {}
    X&   f4()       {}
    X*   f5()       {}

Що ви не можете зробити з неповним типом:

  • Використовуйте його як базовий клас

    class Foo : X {} // compiler error!
  • Використовуйте його для оголошення члена:

    class Foo {
        X m; // compiler error!
    };
  • Визначте функції або методи, використовуючи цей тип

    void f1(X x) {} // compiler error!
    X    f2()    {} // compiler error!
  • Використовуйте його методи або поля, насправді намагаючись знеструмити змінну з неповним типом

    class Foo {
        X *m;            
        void method()            
        {
            m->someMethod();      // compiler error!
            int i = m->someField; // compiler error!
        }
    };

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

Наприклад, std::vector<T>вимагає , щоб його параметр був повним типом, а boost::container::vector<T>ні. Іноді, повний тип потрібен лише в тому випадку, якщо ви використовуєте певні функції учасника; це стосуєтьсяstd::unique_ptr<T> , наприклад,

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


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

2
@AndyDent: Щоправда, але споживачеві заголовка потрібно лише включити залежність, яку він фактично використовує, тому це дотримується принципу C ++ "ви платите лише за те, що використовуєте". Але дійсно, користувачеві, який очікує, що заголовок буде окремим, це може бути незручно.
Люк Турей

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

12
+1 за "поставити себе на місце компілятора". Я уявляю, що "компілятор є" з вусами.
PascalVKooten

3
@JesusChrist: Саме так: коли ви передаєте об'єкт за значенням, компілятору необхідно знати його розмір, щоб зробити відповідну маніпуляцію стеком; при передачі вказівника чи посилання компілятору не потрібен розмір чи компонування об'єкта, лише розмір адреси (тобто розмір вказівника), який не залежить від типу, на який вказано.
Люк Турайль

45

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

Це виключає базові класи та все, крім класів, що використовуються через посилання та покажчики.


6
Майже. Ви також можете посилатися на "прості" (тобто непоказні / посилання) неповні типи як параметри або типи повернення у прототипах функцій.
j_random_hacker

Як щодо класів, які я хочу використовувати як членів класу, які я визначаю у файлі заголовка? Чи можу я переслати їх?
Ігор Окс

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

32

Lakos розрізняє використання класу

  1. лише ім'я (для якого достатньо попередньої декларації) та
  2. за розміром (для якого потрібне визначення класу).

Я ніколи не бачив, щоб це вимовлялося більш лаконічно :)


2
Що означає лише ім'я?
Бун

4
@Boon: насмілюся сказати це ...? Якщо ви використовуєте тільки клас ім'я ?
Марк Муц - mmutz

1
Плюс один для Лакоса, Марк
mlvljr

28

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

Приклади:

struct X;              // Forward declaration of X

void f1(X* px) {}      // Legal: can always use a pointer
void f2(X&  x) {}      // Legal: can always use a reference
X f3(int);             // Legal: return value in function prototype
void f4(X);            // Legal: parameter in function prototype
void f5(X) {}          // ILLEGAL: *definitions* require complete types

19

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

Шаблон класу можна пересилати оголошеним як:

template <typename> struct X;

Дотримуючись структури прийнятої відповіді ,

Ось що можна, а що не можна робити.

Що можна зробити з неповним типом:

  • Оголосити члена вказівником або посиланням на неповний тип у шаблоні іншого класу:

    template <typename T>
    class Foo {
        X<T>* ptr;
        X<T>& ref;
    };
  • Оголосити члена вказівником або посиланням на одну з його неповних інстанцій:

    class Foo {
        X<int>* ptr;
        X<int>& ref;
    };
  • Оголосити шаблони функцій або шаблони функцій членів, які приймають / повертають неповні типи:

    template <typename T>
       void      f1(X<T>);
    template <typename T>
       X<T>    f2();
  • Оголосити функції або функції члена, які приймають / повертають одну з її неповних екземплярів:

    void      f1(X<int>);
    X<int>    f2();
  • Визначте шаблони функцій або шаблони функцій членів, які приймають / повертають покажчики / посилання на неповний тип (але без використання його членів):

    template <typename T>
       void      f3(X<T>*, X<T>&) {}
    template <typename T>
       X<T>&   f4(X<T>& in) { return in; }
    template <typename T>
       X<T>*   f5(X<T>* in) { return in; }
  • Визначте функції або методи, які приймають / повертають покажчики / посилання на одну з її неповних екземплярів (але без використання її членів):

    void      f3(X<int>*, X<int>&) {}
    X<int>&   f4(X<int>& in) { return in; }
    X<int>*   f5(X<int>* in) { return in; }
  • Використовуйте його як базовий клас іншого класу шаблонів

    template <typename T>
    class Foo : X<T> {} // OK as long as X is defined before
                        // Foo is instantiated.
    
    Foo<int> a1; // Compiler error.
    
    template <typename T> struct X {};
    Foo<int> a2; // OK since X is now defined.
  • Використовуйте його для оголошення члена іншого шаблону класу:

    template <typename T>
    class Foo {
        X<T> m; // OK as long as X is defined before
                // Foo is instantiated. 
    };
    
    Foo<int> a1; // Compiler error.
    
    template <typename T> struct X {};
    Foo<int> a2; // OK since X is now defined.
  • Визначте шаблони або методи функцій за допомогою цього типу

    template <typename T>
      void    f1(X<T> x) {}    // OK if X is defined before calling f1
    template <typename T>
      X<T>    f2(){return X<T>(); }  // OK if X is defined before calling f2
    
    void test1()
    {
       f1(X<int>());  // Compiler error
       f2<int>();     // Compiler error
    }
    
    template <typename T> struct X {};
    
    void test2()
    {
       f1(X<int>());  // OK since X is defined now
       f2<int>();     // OK since X is defined now
    }

Що ви не можете зробити з неповним типом:

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

    class Foo : X<int> {} // compiler error!
  • Скористайтеся однією з його примірників, щоб оголосити члена:

    class Foo {
        X<int> m; // compiler error!
    };
  • Визначте функції або методи, використовуючи одну з його інстанцій

    void      f1(X<int> x) {}            // compiler error!
    X<int>    f2() {return X<int>(); }   // compiler error!
  • Використовуйте методи або поля однієї з його інстанцій, насправді намагаючись знеструмити змінну з неповним типом

    class Foo {
        X<int>* m;            
        void method()            
        {
            m->someMethod();      // compiler error!
            int i = m->someField; // compiler error!
        }
    };
  • Створіть явні екземпляри шаблону класу

    template struct X<int>;

2
"Жодна з відповідей поки що не описує, коли можна передати декларацію шаблону класу." Це не просто тому, що семантика Xі X<int>абсолютно однакова, і лише синтаксис, що оголошує вперед, відрізняється будь-яким змістовно, причому всі, крім 1 рядка вашої відповіді, означають просто прийняття Люка і s/X/X<int>/g? Це справді потрібно? Або я пропустив крихітну деталь, яка відрізняється? Це можливо, але я візуально порівняв кілька разів і не бачу жодної ...
underscore_d

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

4

У файлі, в якому ви використовуєте лише вказівник або посилання на клас.

з class Foo;// форвардним оголошенням

Ми можемо оголосити членів даних типу Foo * або Foo &.

Ми можемо оголосити (але не визначити) функції з аргументами та / або повернути значення типу Foo.

Ми можемо оголосити статичні дані членами типу Foo. Це пояснюється тим, що статичні члени даних визначаються поза визначенням класу.


4

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

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

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

Коли ви повертаєте неповний тип, X f2();тоді ви говорите, що ваш абонент повинен мати повну специфікацію типу X. Їм потрібна для створення LHS або тимчасового об'єкта на сайті виклику.

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

class X;  // forward for two legal declarations 
X returnsX();
void XAcceptor(X);

XAcepptor( returnsX() );  // X declaration needs to be known here

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

За винятком

  1. Якщо ця зовнішня залежність є бажаною поведінкою. Замість використання умовної компіляції у вас може бути добре задокументована вимога, щоб вони надавали власний заголовок, декларується X. Це альтернатива використанню #ifdefs і може бути корисним способом введення макетів або інших варіантів.

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


"Я думаю, що є важливим принципом, що заголовок повинен надавати достатню кількість інформації для його використання без залежності, яка вимагає інших заголовків." - інше питання згадується в коментарі Адріана Маккарті на відповідь Навіна. Це забезпечує обгрунтовану причину не дотримуватися вашого принципу "має надавати достатню кількість інформації для використання", навіть для тих, що не є шаблонами.
Тоні Делрой

3
Ви говорите про те, коли слід (або не слід) використовувати декларацію вперед. Але це зовсім не суть цього питання. Йдеться про знання технічних можливостей, коли (наприклад) хочеться розбити проблему кругової залежності.
JonnyJD

1
I disagree with Luc Touraille's answerТому напишіть йому коментар, включаючи посилання на допис у блозі, якщо вам потрібна довжина. Це не відповідає на поставлене запитання. Якби кожен замислювався над питаннями про те, як X працює виправдано, не погоджуючись з тим, що X робить це, або дискутуючи межі, в яких ми повинні обмежувати нашу свободу використання X - у нас майже немає реальних відповідей.
підкреслити_

3

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


2
Це порушує інкапсуляцію і робить код крихким. Для цього вам потрібно знати, чи є тип typedef або клас для шаблону класу з шаблонами шаблонів за замовчуванням, і якщо реалізація колись зміниться, вам потрібно буде оновити будь-коли місце, де ви використовували попередню декларацію.
Адріан Маккарті

@AdrianMcCarthy має рацію, і розумним рішенням є створення заголовка прямого декларування, який міститься в заголовку, вміст якого він оголошує вперед, яким слід володіти / підтримувати / відправляти хто є власником цього заголовка. Наприклад: заголовок бібліотеки iosfwd Standard, який містить прямі декларації вмісту iostream.
Тоні Делрой

3

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


0

Зазвичай ви хочете використовувати переадресацію у файлі заголовка класів, коли ви хочете використовувати інший тип (клас) як член класу. Ви не можете використовувати проголошені вперед файли методів у файлі заголовка, оскільки C ++ ще не знає визначення цього класу на той момент. Це логіка, що вам потрібно перейти до .cpp-файлів, але якщо ви використовуєте шаблон-функції, ви повинні звести їх лише до тієї частини, яка використовує шаблон, і перемістити цю функцію в заголовок.


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

0

Вважайте, що вперед-декларація отримає ваш код для компіляції (створюється obj). Однак посилання (створення EXE) не буде успішним, якщо не знайдеться визначення.


2
Чому коли-небудь 2 особи схвалювали це? Ви не говорите про те, про що йдеться. Ви маєте на увазі нормальне - не вперед - оголошення функцій . Питання стосується попереднього декларування класів . Як ви сказали, "вперед-декларація отримає ваш код для складання", зробіть мені послугу: компілюйте class A; class B { A a; }; int main(){}, і дайте мені знати, як це відбувається. Звичайно, він не збирається. Усі відповідні відповіді пояснюють, чому і точні, обмежені контексти, в яких дійсна декларація є дійсною. Ви натомість написали це про щось зовсім інше.
підкреслюй_d

0

Я просто хочу додати одну важливу річ, яку ви можете зробити з перенаправленим класом, який не згадується у відповіді Люка Туреля.

Що можна зробити з неповним типом:

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

void  f6(X*)       {}
void  f7(X&)       {}
void  f8(X* x_ptr, X& x_ref) { f6(x_ptr); f7(x_ref); }

Модуль може переходити через об'єкт прямо оголошеного класу до іншого модуля.


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

0

Люк Турель вже дуже добре пояснив, де використовувати та не використовувати прямої декларації класу.

Я просто додам до того, чому нам це потрібно використовувати.

Нам слід використовувати декларацію вперед, де це можливо, щоб уникнути небажаної ін'єкції залежності.

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

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