Jędrzej Dudkiewicz
Jędrzej Dudkiewicz

Reputation: 1122

Generating virtual methods in template base class for optional override in inheriting class

What I want is a template class that, given tuple, and used as a base class, provides method with default behavior for each type of element in tuple. These methods should be virtual, so that they could be overridden in inheriting class.

Code below does exactly that:

#include <tuple>
#include <iostream>

struct A {};
struct B {};
struct C {};

template <typename Class, uint16_t tag>
struct def {
        using message_type = Class;
        static constexpr uint16_t class_tag = tag;
};

// (3) adding "constexpr" or "const" causes compilation failure
auto t = std::make_tuple(def<A, 0>(), def<B, 1>(), def<C, 2>());

template <typename T> // (1)
struct base_handler_t {
        virtual void h(T const& t) { std::cout << "base_handler_t\n"; }
};

template <typename ...Ts> // (2) - adding "const" to "std::tuple<Ts...>" in line below makes code work again if "t" is constant
struct base_handler_t<std::tuple<Ts...>> : public base_handler_t<typename Ts::message_type>...{
        using base_handler_t<typename Ts::message_type>::h...;
};

struct service_t : public base_handler_t<decltype(t)> {
        using base_handler_t<decltype(t)>::h;
        void h(B const & b) {
                std::cout << "service_t\n";
        }
};

int main() {
        service_t n;
        n.h(A());
        n.h(B());
}

EDIT AFTER FINDING EXACT AND MINIMAL EXAMPLE THAT BREAKS CODE:

Code above works fine when entered as-is, but if line below comment (3) (about adding constexpr to definition of t) is changed to either:

const auto t = std::make_tuple(def<A, 0>(), def<B, 1>(), def<C, 2>());

or

constexpr auto t = std::make_tuple(def<A, 0>(), def<B, 1>(), def<C, 2>());

code fails to compile. Compiler claims that:

x.cc: In function ‘int main()’:
x.cc:35:16: error: no matching function for call to ‘service_t::h(A)’
   35 |         n.h(A());
      |                ^
x.cc:28:14: note: candidate: ‘void service_t::h(const B&)’
   28 |         void h(B const & b) {
      |              ^
x.cc:28:26: note:   no known conversion for argument 1 from ‘A’ to ‘const B&’
   28 |         void h(B const & b) {
      |                ~~~~~~~~~~^
x.cc:18:22: note: candidate: ‘void base_handler_t<T>::h(const T&) [with T = const std::tuple<def<A, 0>, def<B, 1>, def<C, 2> >]’
   18 |         virtual void h(T const& t) { std::cout << "base_handler_t\n"; }
      |                      ^
x.cc:18:33: note:   no known conversion for argument 1 from ‘A’ to ‘const std::tuple<def<A, 0>, def<B, 1>, def<C, 2> >&’
   18 |         virtual void h(T const& t) { std::cout << "base_handler_t\n"; }
      |                        ~~~~~~~~~^

I assumed that when it comes to templates, there is no real difference if type is extracted from constant or variable.

Code starts to work after changing in line below (2):

struct base_handler_t<std::tuple<Ts...>> : ...

to

struct base_handler_t<const std::tuple<Ts...>> : ...

Why is it so? Is it because std::tuple<Ts...> does not match const std::tuple<Ts...> exactly? What are exact rules that govern this case?

Thanks in advance.

ORIGINAL PLEAD FOR HELP:

In original code base methods for each type (A, B and C in example above) are defined in "service_t" class by hand. I attempted to solve this problem exactly as in example above. As long as all methods were present, code still worked fine. As soon as I commented out single method I received error that there is no matching method to call, followed by list of possible matches. There was match for method with argument std::tuple<def<.... and so on - it seems that while in my snippet above everything works fine (so one method is generated for each tuple element type), there's something preventing larger code base from matching template (2) and instead it uses template (1).

I'd like to hear any idea why this would fail. Thanks in advance.

Upvotes: 0

Views: 108

Answers (1)

Jarod42
Jarod42

Reputation: 217075

With

auto t = std::make_tuple(def<A, 0>(), def<B, 1>(), def<C, 2>());

decltype(t) is std::tuple<def<A, 0>, def<B, 1>, def<C, 2>>

If t is const qualified: const auto t = /*..*/, then, decltype(t) is const std::tuple<def<A, 0>, def<B, 1>, def<C, 2>>.

So for base_handler_t<decltype(t)>, base_handler_t<const std::tuple<def<A, 0>, def<B, 1>, def<C, 2>>> match only primary template definition, not your specialization.

You might use instead base_handler_t<std::remove_const_t<decltype(t)>> or base_handler_t<std::decay_t<decltype(t)>> (remove reference and then cv qualifiers)

Upvotes: 1

Related Questions