Работа с циклическими включениями в контексте циклических ссылок

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

В частности, поскольку я хочу отделить реализацию от определения, у меня будут server.h и server.cpp для класса Server и client.h и client.cpp для класса Client. Поскольку Сервер хранит ссылку на Клиент и вызывает у него методы, ему необходимо #include "client.h". Кроме того, Клиент хранит ссылку на Сервер и вызывает у него методы, ему нужно #include "server.h". На данный момент, даже если я использую защиту заголовков как в server.h, так и в client.h, это все равно мешает (да, это ожидаемо), поэтому я решил предварительно объявить класс Server в client.h и класс Client в server. .час. К сожалению, этого недостаточно для решения проблемы, потому что я также вызываю методы из двух классов, поэтому мне удалось скомпилировать и запустить его (насколько я могу судить, правильно), включив server.h в клиенте .cpp и client.h в server.cpp.

Вышеприведенный «хак» звучит разумно? Стоит ли ожидать каких-то непредвиденных последствий? Есть ли какой-нибудь «умный» способ сделать это без необходимости реализации прокси-класса?

Вот рудиментарный пример того, как будет выглядеть реализация:

файл клиент.h:

#ifndef CLIENT_H
#define CLIENT_H

#include <iostream>
#include <memory>

class Server;

class Client
{
private:
    std::shared_ptr<const Server> server;

public:
    Client () {}

    void setServer (const std::shared_ptr<const Server> &server);

    void doStuff () const;

    void doOtherStuff () const;
};

#endif

файл client.cpp:

#include "client.h"
#include "server.h"

void Client::setServer (const std::shared_ptr<const Server> &server)
{
    this->server = server;
}

void Client::doStuff () const
{
    this->server->doStuff();
}

void Client::doOtherStuff () const
{
    std::cout << "All done!" << std::endl;
}

файл server.h:

#ifndef SERVER_H
#define SERVER_H

#include <iostream>
#include <memory>

class Client;

class Server
{
private:
    std::weak_ptr<const Client> client;

public:
    Server () {}

    void setClient (const std::weak_ptr<const Client> &client);

    void doStuff () const;
};

#endif

файл север.cpp:

#include "server.h"
#include "client.h"

void Server::setClient (const std::weak_ptr<const Client> &client)
{
    this->client = client;
}

void Server::doStuff () const
{
    this->client.lock()->doOtherStuff();
}

файл main.cpp:

#include <iostream>
#include <memory>

#include "client.h"
#include "server.h"

int main ()
{
    std::shared_ptr<Client> client(new Client);
    std::shared_ptr<Server> server(new Server);

    client->setServer(server);
    server->setClient(client);

    client->doStuff();

    return 0;
}

person Mihai Todor    schedule 22.01.2013    source источник
comment
Для меня выглядит нормально, я бы не назвал это взломом.   -  person Kimi    schedule 22.01.2013
comment
Часто так дела и делаются.   -  person Charles Beattie    schedule 22.01.2013


Ответы (3)


«Хак» - это не так, совершенно обычной практикой является разделение объявления и реализации двух классов, как вы это сделали. И совершенно нормально, что *.cpp включает оба заголовка.


Примечание: сначала рассмотрите разные подписи для ваших методов setServer и setClient: в обоих методах вы копируете аргумент. Обе копии нетривиальны, так как нужно обновить use_counts и/или weak_count. Если аргумент действительно является существующим аргументом, это нормально, но если это временный аргумент, копия увеличит счетчик, а уничтожение временного объекта снова уменьшит его каждый раз, когда внутренний указатель должен быть разыменован. Напротив, перемещение shared_ptr или weak_ptr не влияет на счетчики использования, но сбрасывает временные значения. Уничтожение этого временного сброса снова не влияет на счетчик использования (по сути, это нулевой указатель). Во-вторых, всегда предпочитайте make_shared простому new, потому что это экономит вам одно выделение. Поэтому используйте эту реализацию вместо этого:

void Client::setServer (std::shared_ptr<const Server> server)
{
    this->server = std::move(server);
}

int main ()
{
    auto client = std::make_shared<Client>(); //prefer make_shared
    auto server = std::make_shared<Server>();
    /* 1 */
    client->setServer(server); //by copy, if you need to continue to use server
    /* 2 */
    server->setClient(std::move(client)); //by moving
}

Вызов 1 будет таким же дорогим, как и был, вы делаете одну копию shared_ptr, только на этот раз вы делаете это при передаче аргумента, а не внутри метода. Вызов 2 будет дешевле, потому что shared_ptr перемещается и никогда не копируется.


Мое следующее утверждение неверно (см. комментарии) и относится только к unique_ptr, а не к shared_ptr:

Но: поскольку вы используете std::shared_ptr<const Server> в Client, вам придется определить деструктор Client внутри client.cpp. Причина в том, что если вы этого не сделаете, компилятор сгенерирует его за вас, вызвав деструктор shared_ptr и, следовательно, деструктор Server, который не был объявлен внутри client.h. При достаточно высоких уровнях предупреждений ваш компилятор должен жаловаться на вызов удаления для указателя неопределенного класса.

person Arne Mertz    schedule 22.01.2013
comment
Нет, деструктор shared_ptr выполняется через объект удаления, который создается во время присоединения указателя к общему указателю и вызывается через виртуальный вызов, поэтому фактически он не пытается удалить указатель. Это то, что происходит в режиме boost, и я уверен, что это произойдет и в стандартном режиме. - person CashCow; 22.01.2013
comment
@Gorpik нет, ваше редактирование неверно, Арне имел в виду, что ~Client() должен быть объявлен в заголовке, но определен в .cpp. Однако в этом нет необходимости по причинам, которые я объяснил. - person CashCow; 22.01.2013
comment
@CashCow Мне потребовалось несколько прочтений, но я наконец понял. У меня должен быть медленный день. Арне, извините за неправильное редактирование. - person Gorpik; 22.01.2013
comment
@CashCow Я перешел к стандарту, и §20.7.2.2 согласен с вашим первым комментарием. - person Gorpik; 22.01.2013
comment
@CashCow извините, вы правы - я перепутал это с unique_ptr<T>, где используется std::default_delete и где средство удаления не скрыто стиранием типа. Спасибо за разъяснения. - person Arne Mertz; 22.01.2013
comment
Спасибо всем за полезную информацию! - person Mihai Todor; 22.01.2013

Это выглядит хорошо для меня. Прямое объявление server в client.h и прямое объявление client в server.h — это правильно.

Затем можно включить оба файла заголовков в файл .c или .cpp — все, что вам нужно избегать, — это включать файлы заголовков в круг.

person Mats Petersson    schedule 22.01.2013

Вышеприведенный «хак» звучит разумно? Стоит ли ожидать каких-то непредвиденных последствий? Есть ли какой-нибудь «умный» способ сделать это без необходимости реализации прокси-класса?

Форвардное объявление и использование include directive to — это нормальный и правильный способ разорвать циклическое включение.

person billz    schedule 22.01.2013