Когда я могу использовать форвардную декларацию?

Я ищу определение того, когда мне разрешено выполнять форвардное объявление класса в файле заголовка другого класса:

Мне разрешено делать это для базового класса, для класса, содержащегося в качестве члена, для класса, переданного функции-члена по ссылке и т.д.?

Ответ 1

Поставьте себя в положение компилятора: когда вы пересылаете объявление типа, все, что знает компилятор, это то, что этот тип существует; он ничего не знает о его размере, членах или методах. Вот почему он называется неполным типом. Следовательно, вы не можете использовать тип для объявления члена или базового класса, поскольку компилятор должен знать макет типа.

Предполагая следующую предварительную декларацию.

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

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

Ответ 2

Главное правило состоит в том, что вы можете только forward-declare классы, чей макет памяти (и, следовательно, функции-члены и члены данных) не обязательно должны быть известны в файле, который вы пересылаете, объявите его.

Это исключает базовые классы и все, кроме классов, используемых с помощью ссылок и указателей.

Ответ 3

Лакос различает использование классов

  1. только для имени (для которого достаточно предварительного объявления) и
  2. in-size (для которого требуется определение класса).

Я никогда не видел, чтобы это произносили более кратко :)

Ответ 4

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

Примеры:

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

Ответ 5

Ни один из ответов до сих пор не описывает, когда можно использовать форвардное объявление шаблона класса. Итак, вот оно.

Шаблон класса может быть перенаправлен как:

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>;
    

Ответ 6

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

с class Foo;//forward declare

Мы можем объявлять элементы данных типа Foo * или Foo &.

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

Мы можем объявлять статические элементы данных типа Foo. Это связано с тем, что статические члены данных определяются вне определения класса.

Ответ 7

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

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

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

Когда вы возвращаете неполный тип 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 и может быть полезным способом введения mocks или других вариантов.

  • Важным отличием является то, что некоторые шаблонные методы, в которых вы явно НЕ должны их создавать, упомянули просто так, что кто-то не смущает меня.

Ответ 8

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

Ответ 9

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

Ответ 10

Обычно вы хотите использовать форвардное объявление в заголовочном файле классов, если хотите использовать другой тип (класс) в качестве члена класса. Вы не можете использовать методы, объявленные вперед, в заголовочном файле, потому что С++ пока не знает определения этого класса. Эта логика вам нужно переместить в .cpp файлы, но если вы используете шаблонные функции, вы должны уменьшить их только до той части, которая использует шаблон, и переместить эту функцию в заголовок.

Ответ 11

Возьмите его, чтобы форвардное объявление получило ваш код для компиляции (создается объект obj). Однако связывание (создание exe) не будет успешным, если определения не найдены.

Ответ 12

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

Что вы можете сделать с неполным типом:

Определить функции или методы, которые принимают/возвращают указатели/ссылки на неполный тип и пересылать указатели/ссылки к другой функции.

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

Модуль может проходить через объект прямого объявленного класса другому модулю.

Ответ 13

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

Я просто добавлю к этому, почему мы должны использовать это.

Мы должны использовать декларацию Forward везде, где это возможно, чтобы избежать нежелательного внедрения зависимости.

Поскольку заголовочные файлы #include добавляются к нескольким файлам, поэтому, если мы добавим заголовок в другой заголовочный файл, это добавит нежелательную инъекцию зависимостей в различные части исходного кода, чего можно избежать, добавляя заголовок #include в файлы .cpp везде, где это возможно, а не добавление в другой заголовочный файл и, по возможности, использование декларации класса в заголовочных файлах .h.