Альтернатива пересылке объявлений, если вы не хотите включать #include

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

//-----------------------
// foo.h
//-----------------------
class foo
{
   foo();
   ~foo();
};


//-----------------------
// bar.h
//-----------------------

class foo; // forward declaration

class bar
{
   bar();
   ~bar();

   foo* foo_pointer;
};

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

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

//-----------------------
// foo.h
//-----------------------
class foo
{
   foo();
   ~foo();
};


//-----------------------
// bar.h
//-----------------------

class foo;       // Not enough given the way we declare "foo_object"..
#include "foo.h" // ..instead this is required

class bar
{
   bar();
   ~bar();

   foo foo_object;
};

Итак, я был бы счастлив, если бы кто-нибудь знал альтернативную языковую конструкцию, которую можно было бы использовать здесь, чтобы я мог объявить «foo_object», как показано в примере, но без включения его заголовка.

С уважением

/Роберт

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
10
0
3 532
10
Перейти к ответу Данный вопрос помечен как решенный

Ответы 10

Если вы можете использовать ссылку, вы можете сохранить тот же синтаксис использования. Однако ваша ссылка должна быть инициализирована прямо в конструкторе, поэтому ваш ctor обязательно должен быть определен вне линии. (Вам также нужно будет освободить объект в деструкторе.)

// bar.h
class foo;

class bar {
    foo& foo_;

public:
    bar();
    ~bar();
};

// bar.cc
bar::bar() : foo_(*new foo)
{
    // ...
}

bar::~bar()
{
    // ...
    delete &foo_;
}

Ваш пробег может отличаться. :-)

Вы не можете. Компилятору необходимо знать размер объекта при объявлении класса.

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

Другой альтернативой являются умные указатели, но я полагаю, что технически это все еще указатель.

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

Технически объект-член не так уж отличается от ссылки в том, что касается создания экземпляра при построении, поэтому здесь нет потерь. :-)

Chris Jester-Young 20.11.2008 19:22

Член может быть инициализирован по умолчанию и должным образом (полностью) установлен позже, например, сеттеры. Вы не можете сделать это для референтного члена.

Pieter 20.11.2008 19:28

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

sharkin 20.11.2008 19:30

То, что вы хотите, невозможно сделать на C++. Чтобы сгенерировать код для объекта, вашему компилятору необходимо знать, сколько памяти требуется его классу. Чтобы знать это, он должен знать, сколько памяти требуется для каждого члена класса.

Если вы хотите создать класс типа bar с членом типа foo, компилятор должен знать, насколько велик foo. Единственный способ узнать это, если у него есть определение foo (через #include). В противном случае ваш единственный вариант - использовать прямое объявление foo и указатель или ссылку вместо фактического объекта foo.

Нет никакого способа обойти это.

Лучше всего ограничить, сколько включается, но вы должны включить файл с объявлением класса. Вы можете разделить объявление класса на отдельный заголовок, который, надеюсь, не включает ничего другого. Тогда да, у вас должен быть #include, но вы по-прежнему сохраняете свою иерархию include несколько поверхностной. В конце концов, включение одного файла обходится дешево, только когда иерархия простирается до сотен или тысяч файлов, это начинает мешать ...;)

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

Вы не можете избежать включения foo.h, но вы можете сделать его как можно дешевле. Привычка использовать foward-объявления вместо #inlcudes помогает вам на этом пути.

Ответ принят как подходящий

Просто используйте умный указатель - в этом случае вы даже можете использовать auto_ptr.

//-----------------------
// bar.h
//-----------------------

#include <memory>
class foo;       // Not enough given the way we declare "foo_object"..

class bar
{
public:
   bar();
   ~bar();

   foo &foo_object() { return *foo_ptr; }
   const foo &foo_object() const { return *foo_ptr; }

private:
   auto_ptr<foo> foo_ptr;
};

Вы получаете все преимущества автоматического управления памятью, не зная ничего о foo в bar.h. См. Обертывание элементов данных указателя для рекомендаций Херба Саттера.

Если вы действительно хотите, чтобы построение по умолчанию происходило автоматически, попробуйте следующее:

#include <iostream>
using namespace std;

class Foo;

template <typename T>
class DefaultConstuctorPtr
{
    T *ptr;
    void operator =(const DefaultConstuctorPtr &);
    DefaultConstuctorPtr(const DefaultConstuctorPtr &);

public:
    DefaultConstuctorPtr() : ptr(new T()) {}
    ~DefaultConstuctorPtr() { delete ptr; }

    T *operator *() { return ptr; }
    const T *operator *() const { return ptr; }
};

class Bar
{
    DefaultConstuctorPtr<Foo> foo_ptr;
public:
    Bar() {} // The compiler should really need Foo() to be defined here?
};

class Foo
{
public:
    Foo () { cout << "Constructing foo"; }
};

int main()
{
    Bar bar;
}

Разве для этого еще не требуется инициализация foo_ptr в конструкторе бара?

e.James 20.11.2008 19:54

Я слышал, что автоматические указатели обесцениваются, кто-нибудь еще может получить лучший ответ?

MacX.dmg 20.11.2008 21:56

ролл-сам, как некоторые из нас описали ниже. Даже если стандарт C++ удалит auto_ptr, эти самодельные классы все равно будут работать.

e.James 20.11.2008 22:12

Даже если они не рекомендуют auto_ptr (чего я не слышал о том, чтобы комитет по стандартам делал), C++ x0 все равно будет иметь shared_ptr и weak_ptr.

luke 20.11.2008 23:18

Интересно, что это действительно работает из-за использования шаблонов (то есть, если вы сделаете DefaultConstructorPtr обычным классом с Foo, заменяющим T везде, компилятор будет жаловаться на то, что Foo неизвестен). Я немного поэкспериментировал с этим, и кажется, что вы даже можете переместить определение Foo ниже main (), и оно все еще работает. Кроме того, если вы затем сделаете ctor по умолчанию для Foo закрытым, компилятор конкретно пожалуется, что Foo :: Foo () является закрытым. С другой стороны, если вы добавите другой метод и попытаетесь вызвать его из main (), компилятор пожалуется, что Foo не определено. Продолж ...

Ari 06.05.2009 10:25

Но если вы теперь переместите определение Foo прямо над main () - снова нет проблем. Кажется, что все в шаблоне компилируется после всего остального. Интересно, продиктовано ли такое поведение стандартом или зависит от реализации.

Ari 06.05.2009 10:54

Шаблоны компилируются только наполовину, пока не будут использованы.

Eclipse 06.05.2009 17:32

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

Чтобы предотвратить необходимость в другом #include, вы можете включить этот класс myAuto в заголовок префикса своего проекта или скопировать и вставить его в каждый заголовок (не очень хорошая идея, но это сработает).

template<class T>
class myAuto
{
    private:
        T * obj;

    public:
        myAuto() : obj(new T) {  }
        ~myAuto() { delete obj; }
        T& object() { return *obj; }
        T* operator ->() { return obj; }
};

Вот как вы бы это использовали:

// foo.h:
class foo
{
    public:
        foo();
        ~foo();
        void some_foo_func();
};
//bar.h:
class foo;
class bar
{
    public:
       bar();
       ~bar();
       myAuto<foo> foo_object;
};
//main.cc:
#include "foo.h"
#include "bar.h"

int main()
{
    bar a_bar;

    a_bar.foo_object->some_foo_func();

    return 0;
}

Как утверждали другие, вы не можете этого сделать по причинам, о которых они тоже заявили :) Затем вы сказали, что не хотите заботиться о создании / разрушении членов в классе, содержащем их. Для этого можно использовать шаблоны.

template<typename Type>
struct member {
    boost::shared_ptr<Type> ptr;
    member(): ptr(new Type) { }
};

struct foo;
struct bar {
    bar();
    ~bar();

    // automatic management for m
    member<foo> m;
};

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

Вы также можете использовать идиому pImpl, например:

//-----------------------
// foo.h
//-----------------------
class foo
{
    foo();
    ~foo();
};


//-----------------------
// bar.h
//-----------------------

class foo;

class bar
{
private:
    struct impl;
    boost::shared_ptr<impl> impl_;
public:
    bar();

    const foo& get_foo() const;
};

//-----------------------
// bar.cpp
//-----------------------
#include "bar.h"
#include "foo.h"

struct bar::impl
{
    foo foo_object;
    ...
}

bar::bar() :
impl_(new impl)
{
}

const foo& bar::get_foo() const
{
    return impl_->foo_object;
}

Вы по-прежнему получаете преимущества форвардных объявлений, плюс вы скрываете свою частную реализацию. Изменения в реализации bar не обязательно потребуют компиляции всех исходных файлов, содержащих #include bar.h. Сама структура реализации является автономной в файле .cpp, и здесь вы можете объявлять объекты по своему усмотрению.

У вас небольшое снижение производительности из-за самого pImpl, но в зависимости от приложения это может не иметь большого значения.

Я использовал идиому pImpl для больших проектов, и время компиляции очень важно. Жаль, что язык не может справиться с по-настоящему частной реализацией, но вот она.

На самом деле есть только три альтернативы для связывания двух объектов. Вы уже обнаружили два: встроить Foo в Bar или поместить Foo в кучу и поместить Foo * в Bar. Первый требует определения класса Foo перед определением класса Bar; второй просто требует, чтобы вы переадресовали объявление класса Foo.

Существует третий вариант, о котором я упоминаю только потому, что вы специально исключаете оба предыдущих варианта в своем вопросе. Вы можете (в своем .cpp) создать статический std :: map. В каждом конструкторе Bar вы добавляете к этой карте Foo с ключом this. Затем каждый участник бара может найти соответствующий Foo, просмотрев this на карте. Bar :: ~ Bar вызовет erase(this), чтобы уничтожить Foo.

Хотя при этом sizeof (Bar) остается неизменным, фактическое использование памяти выше, чем при включении Foo * в Bar. Тем не менее, вы все равно можете сделать это, если двоичная совместимость является насущной проблемой.

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