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

516

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

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

  • 10
    Я отчаянно хочу, чтобы это было переименовано "когда я должен ", и ответы обновлены соответственно ...
  • 9
    @deworde Когда вы говорите «когда», вы спрашиваете мнение.
Показать ещё 4 комментария
Теги:
forward-declaration
c++-faq

12 ответов

843
Лучший ответ

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

Предполагая следующее форвардное объявление.

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

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

  • 0
    если мы включим заголовочный файл класса, сможем ли мы использовать его как тип членов и т. д. верно? Если мы включим только заголовочный файл класса, этого будет достаточно? и, таким образом, будет ли ненужным предварительное объявление?
  • 4
    Отличный ответ, но, пожалуйста, смотрите мой ниже для инженерной точки, с которой я не согласен. Короче говоря, если вы не включите заголовки для неполных типов, которые вы принимаете или возвращаете, вы навязываете невидимую зависимость от потребителя вашего заголовка, который должен знать, какие другие им нужны.
Показать ещё 20 комментариев
38

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

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

  • 6
    Почти. Вы также можете ссылаться на «незавершенные» (то есть не указатель / ссылка) неполные типы как параметры или возвращаемые типы в прототипах функций.
  • 0
    Как насчет классов, которые я хочу использовать в качестве членов класса, который я определяю в заголовочном файле? Могу ли я отправить их вперед?
Показать ещё 1 комментарий
31

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

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

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

  • 2
    Что означает только имя?
  • 4
    @Boon: смею ли я сказать это ...? Если вы используете только имя класса?
Показать ещё 1 комментарий
23

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

Примеры:

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
9

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

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

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 ? Это действительно нужно? Или я пропустил крошечную деталь, которая отличается? Это возможно, но я несколько раз визуально сравнил и ничего не вижу ...
  • 0
    @underscore_d, спасибо за прод.
Показать ещё 1 комментарий
5

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

с class Foo;//forward declare

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

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

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

3

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

2

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

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

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

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

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

  • 0
    «Я думаю, что есть важный принцип, что заголовок должен предоставлять достаточно информации, чтобы использовать его без зависимости, требующей других заголовков». - другая проблема упоминается в комментарии Адриана Маккарти к ответу Навина. Это дает вескую причину не следовать вашему принципу «следует предоставить достаточно информации для использования», даже для типов без шаблонов.
  • 2
    Вы говорите о том, когда вы должны (или не должны) использовать прямое указание. Это не совсем вопрос этого вопроса. Речь идет о знании технических возможностей, когда (например) вы хотите решить проблему циклической зависимости.
Показать ещё 1 комментарий
2

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

  • 2
    Это нарушает инкапсуляцию и делает код хрупким. Для этого вам нужно знать, является ли тип определением типа или классом для шаблона класса с параметрами шаблона по умолчанию, и, если реализация когда-либо изменится, вам нужно будет обновлять любое место, где вы использовали предварительное объявление.
  • 0
    @AdrianMcCarthy прав, и разумное решение состоит в том, чтобы иметь заголовок прямой декларации, который включается заголовком, чей контент он объявляет, который должен принадлежать / поддерживаться / отправляться тем, кто также владеет этим заголовком. Например: стандартный заголовок библиотеки iosfwd, который содержит предварительные объявления содержимого iostream.
0

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

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

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

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

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

  • 0
    «перенаправленный класс» и «объявленный заранее класс» могут быть ошибочно связаны с двумя совершенно разными вещами. То, что вы написали, следует непосредственно из концепций, неявных в ответе Люка, поэтому, хотя он дал бы хороший комментарий, добавив явное разъяснение, я не уверен, что он оправдывает ответ.
0

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

  • 2
    Почему 2 человека проголосовали за это? Вы не говорите о том, о чем идет речь. Вы имеете в виду нормальное, а не прямое объявление функций . Вопрос о форвард-объявлении классов . Как вы сказали, «предварительное объявление получит ваш код для компиляции», сделайте мне одолжение: скомпилируйте class A; class B { A a; }; int main(){} , и дайте мне знать, как это происходит. Конечно, это не скомпилируется. Все правильные ответы здесь объяснить , почему и точные, ограниченные условия , в которых предобъявление действительно. Вы вместо этого написали это о чем-то совершенно ином.
0

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

  • 0
    Это не имеет никакого смысла. Нельзя иметь член неполного типа. Объявление любого класса должно содержать все, что нужно знать всем пользователям о его размере и компоновке. Его размер включает размеры всех его нестатических элементов. Объявленный вперед участник оставляет пользователей без понятия о его размере.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню