I have a pair of types sharing a loose client/server relationship. The types are nontrivial, so to make them unit testable, I've split them along aspects using decorator. Each decorator adds one aspect of functionality, like locking APIs with a mutex, automatic registration, retrying operations on failure, things like that. The decorator chain doesn't change at runtime, so I've been trying to express it using crtp and devirtualized runtime poly.
I've found a case where I think gcc should be able to devirtualize, but it only does partial. It does manage to devirtualize if I help with a workaround, defining forwarding methods directly in the leaf types. Clang devirtualizes without the workaround.
Is this something that I'm doing wrong, maybe invoking UB, or is it gcc?
In the example below, the root types are undecorated, decorated types decorate the roots, and leaf types are final. The roots parameterize on the leaves for crtp, decorators further parameterize on what they decorate, and the leaves are unparameterized, meant for production. All of the APIs here are in terms of the leaf types.
gcc only devirtualizes when ENABLE_WORKAROUND
is defined.
godbolt link: https://godbolt.org/z/V85CoZ
#include <cstdint>
// #define ENABLE_WORKAROUND
template <typename leaf_client>
class root_server
{
public:
virtual auto associate(leaf_client& client) noexcept -> void { client_ = &client; }
virtual auto on_operation(leaf_client& client) noexcept -> std::intptr_t
{
return reinterpret_cast<std::intptr_t>(&client);
}
private:
leaf_client* client_{};
};
template <typename leaf_client, typename leaf_server, typename undecorated_server>
class decorated_server : public undecorated_server
{
public:
auto associate(leaf_client& client) noexcept -> void override
{
undecorated_server::associate(client);
client.on_associate(static_cast<leaf_server&>(*this));
}
auto on_operation(leaf_client& client) noexcept -> std::intptr_t override
{
return undecorated_server::on_operation(client);
}
};
template <typename leaf_client, typename leaf_server>
class root_client
{
public:
virtual auto on_associate(leaf_server& server) noexcept -> void { server_ = &server; }
virtual auto operation() -> std::intptr_t { return server_->on_operation(static_cast<leaf_client&>(*this)); }
private:
leaf_server* server_{};
};
template <typename leaf_client, typename leaf_server, typename undecorated_client>
class decorated_client : public undecorated_client
{
public:
auto on_associate(leaf_server& server) noexcept -> void override { undecorated_client::on_associate(server); }
auto operation() -> std::intptr_t override { return undecorated_client::operation(); }
};
class leaf_client;
class leaf_server;
using base_server = decorated_server<leaf_client, leaf_server, root_server<leaf_client>>;
class leaf_server final : public base_server
{
public:
#if defined ENABLE_WORKAROUND
auto on_operation(leaf_client& client) noexcept -> std::intptr_t override
{
return base_server::on_operation(client);
}
#endif
};
using base_client = decorated_client<leaf_client, leaf_server, root_client<leaf_client, leaf_server>>;
class leaf_client final : public base_client
{
public:
#if defined ENABLE_WORKAROUND
auto on_associate(leaf_server& server) noexcept -> void override { return base_client::on_associate(server); }
#endif
};
auto main() -> int
{
auto server = leaf_server{};
auto client = leaf_client{};
server.associate(client);
return client.operation();
}