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

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

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

12 ответов

Решение

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

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

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>, например.

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

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

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

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

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

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

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

Примеры:

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

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

с 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

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

Кроме

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Другие вопросы по тегам