Вопрос

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

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

Это было полезно?

Решение

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

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

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

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

Другие советы

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

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

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

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

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

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

Примеры:

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 и может быть полезным способом введения mocks или других вариантов.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top