3
\$\begingroup\$

I'm implementing a C++ layer on top of the sockets api of the OS (i.e. man 7 socket)1. Tcp sockets go through various states. Using the RAII principle leads to distinguishing the states: unconnected2, bound_to_address, listening and connected. To implement this I use move semantics and constructed to move construct on class of object from an other one:

Implementation

Common base class Socket:

class Socket {
    public:
        explicit Socket(int filedescriptor);
        Socket(Socket&& other);
        Socket& operator=(Socket&& other);
        virtual ~Socket();

        int getFiledescriptor() const;

    private:
        Socket() = delete;
        // Sockets can only be moved copying is not allowed
        Socket(const Socket&) = delete;
        Socket& operator=(const Socket&) = delete;

        static const int NO_FILE = -2;
        private std::atomic<int> filedescriptor;
};

Some of Socket's methods implemented:

Socket::Socket(int filedescriptor_)
    : filedescriptor { filedescriptor_ }
{
    // intentionally left blank
}

Socket::Socket(Socket&& other)
    : filedescriptor { other.filedescriptor.exchange(NO_FILE) }
{
    // intentionally left blank
}

Socket::~Socket()
{
    if (filedescriptor >= 0)
    {
        close(filedescriptor);
        // handle error
    }
}

UnconnectedSocket represents opened sockets that are not yet connected to an other socket or bound to an address:

class UnconnectedSocket: public Socket
{
    public:
        UnconnectedSocket();
        UnconnectedSocket(UnconnectedSocket&& other);
        UnconnectedSocket& operator=(UnconnectedSocket&& other);
        virtual ~UnconnectedSocket();

    private:
        UnconnectedSocket(const UnconnectedSocket&) = delete;
        UnconnectedSocket& operator=(UnconnectedSocket&) = delete;
};

The default constructor calls int socket(int domain, int type, int protocol):

namespace {
    int createSocket()
    {
        int fd = socket( AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0 );
        // check for errors and handle them
        return fd;
    }
}

UnconnectedSocket::UnconnectedSocket()
    : Socket { createSocket() }
{
    // intentionally left blank
}

A BoundSocket is a socket bound to a specific address:

class BoundSocket: public Socket {
    public:
        BoundSocket(UnconnectedSocket&& sock, int port);
        BoundSocket(int port);
        BoundSocket(BoudnSocket&& other);
        BoundSocket& operator=(BoundSocket&& other);
        virtual ~BoundSocket();

    private:
        BoundSocket() = delete;
        BoundSocket(const BoundSocket&) = delete;
        BoundSocket& operator=(BoundSocket&) = delete;
};

Implementation:

namespace {
    void bindSocket(int fd, int port)
    {
        sockaddr_in addr { AF_INET, htons(port), INADDR_ANY };
        int result = bind(fd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr));
        // Check for error and handle it
        return result;
    }
}

BoundSocket::BoundSocket(UnconnectedSocket&& sock, int port)
    : Socket(std::move(sock))
{
    bindSocket(getFiledescriptor(), port);
}

BoundSocket::BoundSocket(int port)
    : BoundSocket { UnconnectedSocket { }, port }
{
    // intentionally left blank
}

ListeningSocket and ConnectedSocket use follow a similar move–construct pattern respectively from BoundSocket and from UnconnectedSocket.

Questions

  • An alternative would be to have a single Socket class (and no UnconnectedSocket, BoundSocket, …). Which contains a State-object; e.g. the common implementation of the state pattern.

    An objection to using the state pattern is that the operations supported by the socket change with its state, while the state pattern assumes that the interface remains the same.

    Which implementation is preferable in this situation? The "constructor–move" implementation, the state design pattern, or something else?

  • Is constructing objects and then moving them to other classes of objects a good idea? Or should I refrain from contructing the intermediate UnconnectedSocket and BoundSocket objects?

    My rationale is to adhere to the RAII principle and not risk half constructed objects and the complexity of correctly destroying halfly constructed objects.

  • What about calling Socket::Socket(Socket&&) on derived classes?

    Here it works, because the derived classes do not add extra state. Should derived classes always take into account that their base classes can be moved away «right under their nose» or is this something I should avoid? If so, what alternatives do exist in this situation?

  • Any other issues that your review reveals?


1). I'm aware of Boost Asio and probably other libraries exist as well. And that it is probably better to reuse these existing well tested libraries. But I like to code it myself.

2). When call int socket(int domain, int type, int protocol) succeeds while int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) or int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) fails, we end up with a socket filedescriptor (=resource) which must be closed. Therefore unconnected is distinguished as state.

\$\endgroup\$
3
\$\begingroup\$

I feel as if separating the state description/object/variables from the actual operations feels better--personally, I've coded a state machine for a CAD drawing program in an old, domain-specific form of Lisp that's missing a lot of the OOP and RAII concepts from C++, but I still managed to create a good state machine that was easier to debug by passing around a list of parameters that described the state of the state machine, coding in transitions inside the state functions.

I would say that it would be good to parametrize your state into a variable or external structure instead of derived classes--inheritance, dynamic behavior, and polymorphism can be quite hairy if you keep trying to do this. This way, you won't have to create new classes every time you need to expand or fix behavior, and you won't have to look through multiple classes or files when you have a behavior error--instead, I might code the different states as function objects or closures and then go on from there.

Perhaps what you could do is to call different functions based on the current state, and attach references to the resources you need to pass between states; perhaps you could even move from them as well.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.