Я шукаю визначення, коли мені дозволяється робити переадресацію класу в файлі заголовка іншого класу:
Чи дозволено мені це робити для базового класу, для класу, що знаходиться як член, для класу, переданого функції члена за посиланням тощо?
Я шукаю визначення, коли мені дозволяється робити переадресацію класу в файлі заголовка іншого класу:
Чи дозволено мені це робити для базового класу, для класу, що знаходиться як член, для класу, переданого функції члена за посиланням тощо?
Відповіді:
Поставте себе в позиції компілятора: коли ви пересилаєте декларувати тип, все, що компілятор знає, що цей тип існує; вона нічого не знає про її розмір, членів або методи. Ось чому його називають неповним типом . Тому ви не можете використовувати тип для оголошення члена або базового класу, оскільки компілятору потрібно знати компонування типу.
Припускаючи наступну форвардну декларацію.
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>
, наприклад,
Добре задокументований шаблон повинен вказувати у своїй документації всі вимоги його параметрів, у тому числі, чи мають вони бути повні типи чи ні.
Головне правило полягає в тому, що ви можете лише оголосити переадресування класів, розміщення пам’яті яких (а значить, функцій-членів і членів даних) не потрібно знати у файлі, який ви пересилаєте - оголошуйте.
Це виключає базові класи та все, крім класів, що використовуються через посилання та покажчики.
Lakos розрізняє використання класу
Я ніколи не бачив, щоб це вимовлялося більш лаконічно :)
Крім покажчиків та посилань на неповні типи, ви також можете оголосити прототипи функцій, які задають параметри та / або повертають значення, які є неповними типами. Однак ви не можете визначити функцію, яка має параметр або тип повернення, які є неповними, якщо тільки це не вказівник чи посилання.
Приклади:
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
Жодна з відповідей поки що не описує, коли можна використовувати пряму декларацію шаблону класу. Отже, ось це йде.
Шаблон класу можна пересилати оголошеним як:
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>;
X
і X<int>
абсолютно однакова, і лише синтаксис, що оголошує вперед, відрізняється будь-яким змістовно, причому всі, крім 1 рядка вашої відповіді, означають просто прийняття Люка і s/X/X<int>/g
? Це справді потрібно? Або я пропустив крихітну деталь, яка відрізняється? Це можливо, але я візуально порівняв кілька разів і не бачу жодної ...
У файлі, в якому ви використовуєте лише вказівник або посилання на клас.
з class Foo;
// форвардним оголошенням
Ми можемо оголосити членів даних типу Foo * або Foo &.
Ми можемо оголосити (але не визначити) функції з аргументами та / або повернути значення типу Foo.
Ми можемо оголосити статичні дані членами типу Foo. Це пояснюється тим, що статичні члени даних визначаються поза визначенням класу.
Я пишу це як окрему відповідь, а не просто коментар, оскільки я не погоджуюся з відповіддю Люка Туреля не на підставі законності, а на надійному програмному забезпеченні та небезпеці неправильного тлумачення.
Зокрема, у мене виникає проблема із контрактом, що передбачається, про те, що ви очікуєте від користувачів вашого інтерфейсу.
Якщо ви повертаєтесь або приймаєте типи посилань, то ви просто говорите, що вони можуть пройти через вказівник або посилання, які вони, в свою чергу, можуть знати лише через пряму декларацію.
Коли ви повертаєте неповний тип, 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
Я думаю, що є важливим принципом, що заголовок повинен надавати достатньо інформації, щоб використовувати його без залежності, яка вимагає інших заголовків. Це означає, що заголовок повинен бути в змозі бути включеним до блоку компіляції, не викликаючи помилки компілятора при використанні будь-яких функцій, які він оголошує.
За винятком
Якщо ця зовнішня залежність є бажаною поведінкою. Замість використання умовної компіляції у вас може бути добре задокументована вимога, щоб вони надавали власний заголовок, декларується X. Це альтернатива використанню #ifdefs і може бути корисним способом введення макетів або інших варіантів.
Важливою відмінністю є деякі шаблонні прийоми, де від вас явно НЕ очікується, що вони будуть інстанціюватись, згадані просто так, щоб хтось не поскупився зі мною.
I disagree with Luc Touraille's answer
Тому напишіть йому коментар, включаючи посилання на допис у блозі, якщо вам потрібна довжина. Це не відповідає на поставлене запитання. Якби кожен замислювався над питаннями про те, як X працює виправдано, не погоджуючись з тим, що X робить це, або дискутуючи межі, в яких ми повинні обмежувати нашу свободу використання X - у нас майже немає реальних відповідей.
Загальне правило, яке я дотримуюся, - не включати жоден файл заголовка, якщо мені цього не потрібно. Тому, якщо я не зберігаю об'єкт класу як змінну члена свого класу, я його не включатиму, я просто використовуватиму переадресацію.
Поки вам не потрібне визначення (міркувальні покажчики та посилання), ви можете піти з попередніх декларацій. Ось чому в основному ви бачите їх у заголовках, тоді як файли впровадження, як правило, витягуватимуть заголовок для відповідного визначення.
Зазвичай ви хочете використовувати переадресацію у файлі заголовка класів, коли ви хочете використовувати інший тип (клас) як член класу. Ви не можете використовувати проголошені вперед файли методів у файлі заголовка, оскільки C ++ ще не знає визначення цього класу на той момент. Це логіка, що вам потрібно перейти до .cpp-файлів, але якщо ви використовуєте шаблон-функції, ви повинні звести їх лише до тієї частини, яка використовує шаблон, і перемістити цю функцію в заголовок.
Вважайте, що вперед-декларація отримає ваш код для компіляції (створюється obj). Однак посилання (створення EXE) не буде успішним, якщо не знайдеться визначення.
class A; class B { A a; }; int main(){}
, і дайте мені знати, як це відбувається. Звичайно, він не збирається. Усі відповідні відповіді пояснюють, чому і точні, обмежені контексти, в яких дійсна декларація є дійсною. Ви натомість написали це про щось зовсім інше.
Я просто хочу додати одну важливу річ, яку ви можете зробити з перенаправленим класом, який не згадується у відповіді Люка Туреля.
Що можна зробити з неповним типом:
Визначте функції або методи, які приймають / повертають покажчики / посилання на неповний тип і пересилають ці покажчики / посилання на іншу функцію.
void f6(X*) {}
void f7(X&) {}
void f8(X* x_ptr, X& x_ref) { f6(x_ptr); f7(x_ref); }
Модуль може переходити через об'єкт прямо оголошеного класу до іншого модуля.
Люк Турель вже дуже добре пояснив, де використовувати та не використовувати прямої декларації класу.
Я просто додам до того, чому нам це потрібно використовувати.
Нам слід використовувати декларацію вперед, де це можливо, щоб уникнути небажаної ін'єкції залежності.
Оскільки #include
файли заголовків додаються до декількох файлів, тому, якщо ми додаємо заголовок до іншого файлу заголовка, він додасть небажану ін'єкцію залежності у різних частинах вихідного коду, чого можна уникнути, додавши #include
заголовок у .cpp
файли, де це можливо, а не додавати до іншого файлу заголовка та використовувати .h
файли переадресації класу, де це можливо, у файлах заголовків .