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

602

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

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

Игорь Окс
источник
14
Я отчаянно хочу, чтобы это было переименовано «когда я должен », и ответы были соответственно обновлены ...
deworde
12
@deworde Когда вы говорите «когда», вы спрашиваете мнение.
AturSams
@ deworde, насколько я понимаю, вы хотите использовать предварительные объявления, когда это возможно, чтобы сократить время сборки и избежать циклических ссылок. Единственное исключение, о котором я могу думать, это когда включаемый файл содержит typedefs, и в этом случае существует компромисс между переопределением typedef (и риском его изменения) и включением всего файла (вместе с его рекурсивными включениями).
Охад Шнайдер
@OhadSchneider С практической точки зрения, я не большой поклонник заголовков, которые мои. ÷
deworde
в основном всегда требуется, чтобы вы использовали разные заголовки, чтобы использовать их (здесь большой виноват параметр forward decl of constructor)
deworde

Ответы:

962

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

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

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

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

Люк Турэй
источник
4
Отличный ответ, но, пожалуйста, смотрите мой ниже для инженерной точки, с которой я не согласен. Короче говоря, если вы не включите заголовки для неполных типов, которые вы принимаете или возвращаете, вы заставляете невидимую зависимость от потребителя вашего заголовка, который должен знать, какие другие им нужны.
Энди Дент
2
@AndyDent: Верно, но потребителю заголовка нужно только включить зависимости, которые он фактически использует, так что это соответствует принципу C ++ «вы платите только за то, что используете». Но на самом деле, это может быть неудобно для пользователя, который ожидает, что заголовок будет автономным.
Люк Турэй
8
Этот набор правил игнорирует один очень важный случай: вам нужен полный тип для создания экземпляров большинства шаблонов в стандартной библиотеке. На это нужно обратить особое внимание, поскольку нарушение правила приводит к неопределенному поведению и может не вызвать ошибку компилятора.
Джеймс Канз
12
+1 за «поставь себя на позицию компилятора». Я представляю "существо компилятора" с усами.
PascalVKooten
3
@JesusChrist: Точно: когда вы передаете объект по значению, компилятору необходимо знать его размер, чтобы сделать соответствующие манипуляции со стеком; при передаче указателя или ссылки компилятору не требуется размер или расположение объекта, а только размер адреса (то есть размер указателя), который не зависит от указанного типа.
Люк Турай
45

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

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

Тимо Гюш
источник
6
Почти. Вы также можете ссылаться на «незавершенные» (то есть не указатель / ссылка) неполные типы как параметры или возвращаемые типы в прототипах функций.
j_random_hacker
Как насчет классов, которые я хочу использовать в качестве членов класса, который я определяю в заголовочном файле? Могу ли я отправить их вперед?
Игорь Окс
1
Да, но в этом случае вы можете использовать только ссылку или указатель на заранее объявленный класс. Но, тем не менее, он позволяет вам иметь членов.
Reunanen
32

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

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

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

Марк Муц - ммц
источник
2
Что означает только имя?
Благо
4
@Boon: смею ли я сказать это ...? Если вы используете только класс имя ?
Марк Муц - Мамц
1
Плюс один для Лакоса, Марк
mlvljr
28

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

Примеры:

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
j_random_hacker
источник
20

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

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

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? Это действительно нужно? Или я пропустил крошечную деталь, которая отличается? Это возможно, но я несколько раз визуально сравнивал и ничего не вижу ...
underscore_d
Спасибо! Это редактирование добавляет тонну ценной информации. Мне придется прочитать его несколько раз, чтобы полностью понять это ... или, возможно, использовать часто лучшую тактику ожидания, пока я не запутаюсь в реальном коде и не вернусь сюда! Я подозреваю, что смогу использовать это, чтобы уменьшить зависимости в разных местах.
underscore_d
4

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

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

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

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

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

yesraaj
источник
4

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

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

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

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

Энди Дент
источник
1
«Я думаю, что есть важный принцип, что заголовок должен предоставлять достаточно информации, чтобы использовать его без зависимости, требующей других заголовков». - другая проблема упоминается в комментарии Адриана Маккарти к ответу Навина. Это дает вескую причину не следовать вашему принципу «следует предоставить достаточно информации для использования», даже для типов без шаблонов.
Тони Делрой
4
Вы говорите о том, когда вы должны (или не должны) использовать прямое указание. Однако это не совсем так. Речь идет о знании технических возможностей, когда (например) нужно решить проблему циклической зависимости.
JonnyJD
2
I disagree with Luc Touraille's answerТак что напишите ему комментарий, в том числе ссылку на пост в блоге, если вам нужна длина. Это не отвечает на заданный вопрос. Если бы все думали о том, как работает Х, оправданные ответы, не соглашаясь с тем, как это делает Х, или обсуждая пределы, в которых мы должны ограничивать нашу свободу использования Х - у нас почти не было бы реальных ответов.
underscore_d
3

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

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

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

dirkgently
источник
0

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

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

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

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

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

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

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

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

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

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

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

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

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

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

А 786
источник