0

As a follow up to an older question of mine, I wish to implement a client-server mockup simulation, where the client initiates a sequence of actions that involve calling methods on the server, which, in turn, can call methods on the client (let's ignore the issue that the stack may blow up).

More specifically, since I want to split the implementation from the definition, I will have server.h and server.cpp for the Server class and client.h and client.cpp for the Client class. Since Server holds a reference to Client and calls methods from it, it needs to #include "client.h". Also, Client holds a reference to Server and calls methods from it, it needs to #include "server.h". At this point, even if I use header guards in both server.h and client.h, it still messes up (yeah, it's expected) so I decided to forward-declare the Server class in client.h and the Client class in server.h. Unfortunately, this is not enough to solve the issue, because I'm also calling methods from the two classes, so I managed to make it compile & run (properly, as far as I can tell), by including server.h in client.cpp and client.h in server.cpp.

Does the above "hack" sound reasonable? Should I expect some unforeseen consequences? Is there any "smarter" way to do this without having to implement a proxy class?

Here's a rudimentary sample of how the implementation will look like:

file client.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

file 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;
}

file 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

file sever.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();
}

file 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;
}
Community
  • 1
  • 1
Mihai Todor
  • 8,014
  • 9
  • 49
  • 86

3 Answers3

4

That looks good to me. Forward declaring server in the client.h and forward declaring client in the server.h is the right thing to do.

It is perfectly fine to then include both header files in the .c or .cpp file - all you need to avoid is including the header files in a circle.

Mats Petersson
  • 126,704
  • 14
  • 140
  • 227
3

Does the above "hack" sound reasonable? Should I expect some unforeseen consequences? Is there any "smarter" way to do this without having to implement a proxy class?

Forward declaration and use include directive to is the normal and right way to break circular include.

billz
  • 44,644
  • 9
  • 83
  • 100
3

The "Hack" is none, it's perfectly common practice to separate declaration and implementation of the two classes as you did. And it's perfectly normal that the *.cpp include both Headers.


Sidenote: First consider different signatures for your setServer and setClient methods: In both methods, you copy the argument. Both copies are nontrivial, since the use_counts and/or weak_count have to be updated. If the argument indeed is an existing argument, that is ok, but if it is a temporary, the copy will increase the count and destruction of the temporary will decrease it again, each time an internal pointer has to be dereferenced. In contrast, moving a shared_ptr or weak_ptr does not affect the use counts but resets the temporary. Destruction of that reset temporary again does not affect the use count (it effectively is a null pointer). Secondly, always prefer make_shared over simple new, because it saves you one allocation. So use this implementation instead:

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
}

Call 1 will be as expensive at it was, you make one copy pf the shared_ptr, only this time you make it while passing the argument, not inside the method. Call 2 will be cheaper, because the shared_ptr is moved around and never copied.


My following statement is false (see comments) and applies only to unique_ptr, not to shared_ptr:

But: Since you use a std::shared_ptr<const Server> in Client, you will have to define Client's destructor inside client.cpp. The reason is that if you don't, the compiler will generate it for you, calling the shared_ptr's and thus Server's destructor which has not been declared inside client.h. At reasonably high warning levels you compiler should complain about calling delete on a an undefined class' pointer.

Arne Mertz
  • 24,171
  • 3
  • 51
  • 90
  • 3
    No, the destructor of a shared_ptr is performed through the deleter object which is created at the time where the pointer is attached to a shared pointer and is invoked through a virtual call therefore it does not actually try to delete the pointer. That is what happens in boost anyway and I am sure it will happen in std too. – CashCow Jan 22 '13 at 11:46
  • @Gorpik no, your edit is wrong, Arne meant that ~Client() should be declared in the header but defined in the .cpp. However it does not need to be for reasons I explained. – CashCow Jan 22 '13 at 11:50
  • @CashCow It took me several reads, but I finally got it. I must be having a slow day. Arne, sorry for the wrong edit. – Gorpik Jan 22 '13 at 12:02
  • 1
    @CashCow I went to the standard and §20.7.2.2 agrees with your first comment. – Gorpik Jan 22 '13 at 12:14
  • @CashCow sorry, you're right - I confused that with `unique_ptr`, where `std::default_delete` is used and where the deleter is not hidden by type erasure. Thanks for the clarification. – Arne Mertz Jan 22 '13 at 12:56
  • Thanks for the useful insights everyone! – Mihai Todor Jan 22 '13 at 13:26