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

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

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

Ответы [ 12 ]

911 голосов
/ 16 февраля 2009

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

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

class X;

Вот что вы можете и не можете делать.

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

  • Объявление элемента указателем или ссылкой на неполный тип:

    class Foo {
        X *pt;
        X &pt;
    };
    
  • Объявите функции или методы, которые принимают / возвращают неполные типы:

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

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

43 голосов
/ 16 февраля 2009

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

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

32 голосов
/ 21 июля 2009

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

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

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

28 голосов
/ 16 февраля 2009

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

Примеры:

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
15 голосов
/ 31 октября 2015

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

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

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>;
    
5 голосов
/ 16 февраля 2009

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

с class Foo; // предварительное объявление

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

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

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

4 голосов
/ 06 июля 2013

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

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

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

Когда вы возвращаете неполный тип X f2();, вы говорите, что ваш вызывающий должен иметь полную спецификацию типа X. Им это нужно для создания LHS или временного объекта на сайте вызова. .

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

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

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

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

За исключением

  1. Если эта внешняя зависимость является желаемым поведением. Вместо условной компиляции у вас может быть хорошо документированное требование , чтобы они предоставили свой собственный заголовок, объявляющий X. Это альтернатива использованию #ifdefs и может быть полезным способом представить mock или другие варианты.

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

3 голосов
/ 16 февраля 2009

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

3 голосов
/ 16 февраля 2009

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

0 голосов
/ 08 июня 2016

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

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

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

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

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

...