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

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

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


person Igor Oks    schedule 16.02.2009    source источник
comment
Я отчаянно хочу, чтобы это было переименовано, когда следует я, и ответы обновлены соответствующим образом ...   -  person deworde    schedule 08.06.2015
comment
@deworde Когда вы говорите, когда следует, вы спрашиваете мнение.   -  person AturSams    schedule 24.03.2016
comment
@deworde, насколько я понимаю, вы хотите использовать форвардные объявления всякий раз, когда можете, чтобы сократить время сборки и избежать циклических ссылок. Единственное исключение, о котором я могу думать, - это когда включаемый файл содержит typedef, и в этом случае существует компромисс между переопределением typedef (и риском его изменения) и включением всего файла (вместе с его рекурсивными включениями).   -  person Ohad Schneider    schedule 07.12.2016
comment
@OhadSchneider С практической точки зрения я не большой поклонник заголовков, которые принадлежат моему. ÷   -  person deworde    schedule 07.12.2016
comment
в основном всегда требуется, чтобы вы включали другой заголовок, чтобы использовать их (здесь большой виновник является прямым объявлением параметра конструктора)   -  person deworde    schedule 07.12.2016
comment
Поскольку правило циклических зависимостей лучше всего избегать с помощью лучшего дизайна ООП - всегда применяются исключения (например, посетитель); который отвечает на вопрос, когда должен I. Однако исключения из правил - это исключительные случаи, поэтому, чтобы ответить, когда может I; в идеале, редко без обоснования   -  person earcam    schedule 31.03.2017


Ответы (13)


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

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

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

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

person Luc Touraille    schedule 16.02.2009
comment
если мы включим файл заголовка класса, сможем ли мы использовать его в качестве типа членов и т. д., правильно? Если мы включим только файл заголовка класса, этого будет достаточно? и тогда не было бы необходимости в форвардном объявлении? - person Rika; 24.06.2013
comment
Отличный ответ, но, пожалуйста, посмотрите мой ниже инженерный момент, с которым я не согласен. Короче говоря, если вы не включаете заголовки для неполных типов, которые вы принимаете или возвращаете, вы создаете невидимую зависимость от потребителя вашего заголовка, который должен знать, какие другие им нужны. - person Andy Dent; 06.07.2013
comment
@AndyDent: Верно, но потребителю заголовка необходимо включить только те зависимости, которые он фактически использует, поэтому это следует принципу C ++, согласно которому вы платите только за то, что используете. Но на самом деле это может быть неудобно для пользователя, который ожидает, что заголовок будет автономным. - person Luc Touraille; 08.07.2013
comment
Этот набор правил игнорирует один очень важный случай: вам нужен полный тип для создания экземпляров большинства шаблонов в стандартной библиотеке. На это следует обратить особое внимание, поскольку нарушение правила приводит к неопределенному поведению и может не вызвать ошибку компилятора. - person James Kanze; 12.07.2013
comment
+1 за то, что поставил себя на место компилятора. Я представляю, что у компилятора есть усы. - person PascalVKooten; 01.09.2013
comment
А как насчет оператора new? Нужно ли знать полный тип? например X * myFunc(){ return new X; } - person ontherocks; 06.02.2014
comment
@ontherocks: Да, тип должен быть определен. - person Luc Touraille; 07.02.2014
comment
@ontherocks Как и sizeof, он должен знать размер типа. - person Jim Balter; 16.10.2014
comment
Почему нормально работать с указателем или ссылкой на пересылку объявленного класса, но не с самим классом, переданным по значению? Это потому, что размер указателя и ссылки всегда известен во время компиляции, независимо от типа? - person PaulD; 24.11.2014
comment
@JesusChrist: Точно: когда вы передаете объект по значению, компилятор должен знать его размер, чтобы произвести соответствующие манипуляции со стеком; при передаче указателя или ссылки компилятору не нужен размер или макет объекта, а только размер адреса (то есть размер указателя), который не зависит от типа, на который указывает. - person Luc Touraille; 24.11.2014
comment
Могу ли я использовать его для определения функции, возвращающей 'const X *', например. const X* do_something( void )? - person Dilawar; 11.04.2016
comment
@Dilawar: Да, можно, если телу функции не требуется определение X (например, он не может создать X или получить доступ к его членам). - person Luc Touraille; 11.04.2016
comment
Просто комментарий к хорошо задокументированным типам шаблонов, начните с поиска неполных документов в их документации, например, в boost::shared_ptr: . Шаблон класса параметризован по T, типу объекта, на который указывает. shared_ptr и большинство его функций-членов не предъявляют никаких требований к T; он может быть неполным или недействительным. - person jxramos; 29.04.2016
comment
Было бы неплохо добавить комментарий @jxramos в ответ. Это именно то, что мне нужно было знать. - person Tomáš Zato - Reinstate Monica; 25.05.2016
comment
Возможно, стоит отметить, что теперь, когда они существуют, вы также можете сделать это для интеллектуальных указателей. - person Matthew Woo; 03.11.2016
comment
Почему можно объявлять функцию с неполным типом возврата? если он возвращается по значению, разве компилятор не должен знать его структуру памяти? - person Hanna Khalil; 14.11.2016
comment
@HannaKhalil: компилятору необходимо знать структуру своей памяти только на месте вызова, и если вы сохраняете возвращаемое значение в переменной, и в этом случае вам нужно включить определение типа. - person Luc Touraille; 16.11.2016
comment
У меня вопрос: если используется предварительное объявление, то все файлы cpp, включая этот файл заголовка, также должны включать файл заголовка предварительно объявленного файла, является ли это бременем? - person Lyn; 13.01.2017
comment
@LucTouraille: Что, если что-то включает заголовок, но не вызывает функцию? В этом случае технически нет причин для включающего файла ожидать, что он должен также включать тот другой файл, в котором определен возвращаемый тип. Допускает ли стандарт, что тип возвращаемого значения может быть объявлен вперед только в том случае, если он возвращается по значению? - person Craig Scott; 01.02.2017
comment
@CraigScott. Если включаемый файл не вызывает функцию, действительно нет необходимости в определении возвращаемого типа. Пользователям заголовка требуется только определение типов, которые они фактически используют, что ускоряет компиляцию. Неполный тип может использоваться в качестве возвращаемого типа при объявлении функции, но не при определении (реализации) ее или при ее вызове. - person Luc Touraille; 04.02.2017
comment
Люк: (ссылка) as long as the body of the function does not need the definition Я предполагаю, что @Dilawar на самом деле имелось в виду объявить, когда они сказали определить, спрашивая, что их собственная функция может сделать с неполным типом. В этом случае объявление указанной функции, вероятно, находится в заголовке, а определение, вероятно, находится в cpp файле, поэтому последний может #include / определить полный тип и использовать его для всего. - person underscore_d; 13.02.2017
comment
Просто включив файл заголовка и без предварительного объявления класса, я могу создать объект класса как переменную-член. - person ; 09.08.2018
comment
Блестящий ответ, браво. - person Dean P; 20.08.2019

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

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

person Timo Geusch    schedule 16.02.2009
comment
Почти. Вы также можете ссылаться на простые (т.е. не указатели / ссылки) неполные типы как на параметры или возвращаемые типы в прототипах функций. - person j_random_hacker; 16.02.2009
comment
Как насчет классов, которые я хочу использовать в качестве членов класса, который я определяю в файле заголовка? Могу ли я пересылать их декларировать? - person Igor Oks; 16.02.2009
comment
Да, но в этом случае вы можете использовать только ссылку или указатель на заранее объявленный класс. Но, тем не менее, он позволяет вам иметь участников. - person Reunanen; 16.02.2009

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

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

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

person Marc Mutz - mmutz    schedule 21.07.2009
comment
Что означает только имя? - person Boon; 25.12.2014
comment
@Boon: смею я это сказать ...? Если вы используете только класс 'name? - person Marc Mutz - mmutz; 01.01.2015

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

Примеры:

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
person j_random_hacker    schedule 16.02.2009

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

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

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>;
    
person R Sahu    schedule 31.10.2015
comment
Ни один из ответов до сих пор не описывает, когда можно выполнить прямое объявление шаблона класса. Разве это не просто потому, что семантика X и X<int> совершенно одинакова, и только синтаксис прямого объявления существенно отличается, и все, кроме одной строки вашего ответа, равносильны просто взятию Люка и s/X/X<int>/g? Это действительно нужно? Или я пропустил крошечную деталь, которая отличается? Это возможно, но я несколько раз сравнивал визуально и ничего не вижу ... - person underscore_d; 12.08.2016
comment
Спасибо! Это правка добавляет массу ценной информации. Мне придется прочитать его несколько раз, чтобы полностью понять это ... или, может быть, я буду использовать более эффективную тактику: дождаться, пока я ужасно запутаться в реальном коде, и вернуться сюда! Я подозреваю, что смогу использовать это для уменьшения зависимостей в разных местах. - person underscore_d; 13.08.2016

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

с _1 _ // форвардное объявление

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

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

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

person yesraaj    schedule 16.02.2009

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

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

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

Когда вы возвращаете неполный тип 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. Важным отличием является то, что некоторые методы шаблонов, в которых вы явно НЕ ожидаете их создания, упоминаются только для того, чтобы кто-то не стал язвить меня.

person Andy Dent    schedule 06.07.2013
comment
Я думаю, что существует важный принцип, согласно которому заголовок должен предоставлять достаточно информации для его использования без зависимости, требующей других заголовков. - еще одна проблема упоминается в комментарии Адриана Маккарти к ответу Навина. Это дает вескую причину, чтобы не следовать тому, что вы должны предоставить достаточно информации для использования принципа даже для типов, не являющихся в настоящее время шаблонами. - person Tony Delroy; 07.03.2014
comment
Вы говорите о том, когда вы должны (или не должны) использовать форвардное объявление. Однако вопрос не в этом. Речь идет о знании технических возможностей, когда (например) вы хотите разорвать проблему циклической зависимости. - person JonnyJD; 31.07.2014
comment
I disagree with Luc Touraille's answer Так что напишите ему комментарий, включая ссылку на сообщение в блоге, если вам нужна его длина. Это не отвечает на заданный вопрос. Если бы все думали, что вопросы о том, как работает X, оправдывали бы ответы, несогласные с тем, что X делает это, или обсуждали пределы, в которых мы должны ограничивать нашу свободу использования X, у нас почти не было бы реальных ответов. - person underscore_d; 13.08.2016

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

person Naveen    schedule 16.02.2009
comment
Это нарушает инкапсуляцию и делает код хрупким. Для этого вам нужно знать, является ли тип typedef или классом для шаблона класса с параметрами шаблона по умолчанию, и если реализация когда-либо изменится, вам нужно будет обновить любое место, где вы использовали предварительное объявление. - person Adrian McCarthy; 31.05.2013
comment
@AdrianMcCarthy прав, и разумным решением является наличие заголовка предварительного объявления, который включается в заголовок, содержимое которого он объявляет, и который должен принадлежать / поддерживаться / доставляться тому, кто также владеет этим заголовком. Например: заголовок стандартной библиотеки iosfwd, который содержит предварительные объявления содержимого iostream. - person Tony Delroy; 07.03.2014

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

person dirkgently    schedule 16.02.2009

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

person Patrick Glandien    schedule 16.02.2009
comment
Это не имеет никакого смысла. Не может быть члена неполного типа. Объявление любого класса должно содержать все, что нужно знать пользователям о его размере и расположении. Его размер включает размеры всех его нестатических членов. Предварительное объявление участника оставляет пользователей без представления о его размере. - person underscore_d; 12.08.2016

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

person Sesh    schedule 16.02.2009
comment
Почему 2 человека проголосовали за это? Вы не говорите о том, о чем идет речь. Вы имеете в виду обычное, а не прямое объявление функций. Речь идет о форвард-объявлении классов. Как вы сказали, предварительное объявление заставит ваш код скомпилировать, сделайте мне одолжение: скомпилируйте class A; class B { A a; }; int main(){} и дайте мне знать, как это происходит. Конечно, он не компилируется. Все правильные ответы здесь объясняют, почему и в каких точных ограниченных контекстах допустимо прямое объявление . Вместо этого вы написали это о чем-то совершенно другом. - person underscore_d; 12.08.2016

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

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

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

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

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

person Niceman    schedule 08.06.2016
comment
перенаправленный класс и предварительно объявленный класс могут ошибочно относиться к двум очень разным вещам. То, что вы написали, следует непосредственно из концепций, подразумеваемых в ответе Люка, поэтому, хотя это было бы хорошим комментарием, добавляющим явные разъяснения, я не уверен, что это оправдывает ответ. - person underscore_d; 12.08.2016

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

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

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

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

person A 786    schedule 13.07.2019