4

Say I have a functor that uses tag dispatching to select from multiple implementations of the function, something like this:

// base class for all tags, indicating the "default" implementation
struct tag_base { };

// subclasses for tags that might select a different implementation
struct tag1 : tag_base { };
struct tag2 : tag1  { };
struct tag3 : tag2 { };

struct func
{
    void operator()(tag_base) { }
    void operator()(tag3) { }
};

In this simple example, therefore, tag types tag1 and tag2 would dispatch to the call operator for func that takes a tag_base, the default implementation. However, tag3 would instead dispatch to a different implementation. I'm interested in inspecting at compile time whether, for a given function type func and two tag types T and U, whether they would dispatch to the same overload of func's call operator.

Basically, I would like a trait like this (only pseudocode, since this approach does not compile):

template <typename Func, typename T, typename U>
struct has_same_tag_overload
{
    enum { value =  (void (Func::*)(T)) &func::operator() ==
                    (void (Func::*)(U)) &func::operator() };
}

So in the above example, the following would be true:

  • tag<func, tag_base, tag1>::value == 1
  • tag<func, tag_base, tag2>::value == 1
  • tag<func, tag1, tag2>::value == 1
  • tag<func, tag1, tag3>::value == 0
  • tag<func, tag2, tag3>::value == 0

Is this possible? To add to the degree of difficulty, is this possible in C++03?

Jason R
  • 11,159
  • 6
  • 50
  • 81
  • @VTT No it doesn’t, for two reasons: 1. you can’t compare pointers of distinct type for equality, and 2. there is no overload of type `void (func::*)(tag1)`, so the trait doesn’t work with inheritance. – Daniel H Nov 02 '17 at 17:32
  • Sorry, that was a typo when I transferred it over here. gcc complains about the cast being invalid; I would like to invoke the trait using T= tag1 and U = tag2, but since the corresponding overload takes tag_base instead, it says the cast is invalid. – Jason R Nov 02 '17 at 17:33
  • @JasonR Are the functions actually `void`, or is that a simplification for SO? I expect you could do it if you changed the return type to the same as the tag parameter, but I would only know how in C++11 with `decltype` even then. – Daniel H Nov 02 '17 at 17:43
  • @VTT I believe that the checks [here](https://godbolt.org/g/UJdVNT) should all work, except the part where I wrote them with `static_assert`. Checking that the overload exists is only part of that, and we might just be able to assume that and refuse to compile if there’s no relevant overload. – Daniel H Nov 02 '17 at 17:48
  • @DanielH: The functions are not `void`, that was a simplification, but the return types for two different tags are identical in some cases. – Jason R Nov 02 '17 at 18:12
  • Cat tags inherit only from some base classes or from "lower level" tags as well (e.g. `struct tag4: tag2 {}`)? – user7860670 Nov 02 '17 at 18:51
  • @VTT They can inherit from one another, as in your example. – Jason R Nov 02 '17 at 18:51
  • It seems that solving this problem would require figuring out exact signature of the function that may be called with a given argument, that is figuring out possible conversion sequences between tags. So this is hardly possible without explicitly supplying list of tags for which overloads exist. – user7860670 Nov 02 '17 at 18:57
  • @VTT Yes, that's what I thought might be the case. You're right, the question eventually simplifies to the problem of, for a given set of arguments, determining what overload of a function would be invoked (while allowing implicit conversions). I think even if I could do so at runtime, I could benefit in my application, but I haven't figured out a way to do that. – Jason R Nov 02 '17 at 19:03

2 Answers2

3

if the tags form a tree hierarchy, and hence can be written as

struct tag_base { using base = void; };
struct tag1 : tag_base { using base = tag_base; };
struct tag2 : tag1  { using base = tag1; };
struct tag3 : tag1  { using base = tag1; };
...
// or something like tag4: is_a<tag2> {}; if you don't like typing ...

then we could write

template<class U>
struct tag_matcher
{
  template<class V, class W = std::enable_if_t<std::is_same<U,V>::value> >
  operator V();
};

template<typename F, typename T>
std::true_type match_tag_impl( decltype(F{}(tag_matcher<T>()))* );

template<typename F, typename T>
std::false_type match_tag_impl( ... );

template<typename F, typename T>
struct match_tag
{
  using type = std::conditional_t< decltype(match_tag_impl<F,T>(0))::value,
    T, typename match_tag<F, typename T::base>::type >;
};

template<typename F>
struct match_tag<F,void> { using type = void; };

template<typename F, typename T, typename U>
struct has_same_tag_overload:
  std::is_same< typename match_tag<F,T>::type, typename match_tag<F,U>::type > {};

the idea is to check all ancestors in order to find the most derived match, then check if the two are the same or not. This is c++11, but you can make it work for c++03 too as far as I can tell.

update re C++03:

as mentioned in the comment, the tag_matcher as defined above cannot work in c++03, because we have no default function template parameters there, hence making sfinae enabled conversion operator templates impossible. That said, we can move the same logic as tag converting constructors:

template<class U>
struct tag_matcher{};

// macro just for expository purposes
#define MATCHME(TAG) template<class T> TAG( tag_matcher<T>, std::enable_if_t<std::is_same<T,TAG>::value>* = 0 )

struct tag_base { using base = void; MATCHME(tag_base); };
struct tag1 : tag_base { using base = tag_base; MATCHME(tag1); };
// ...
Community
  • 1
  • 1
Massimiliano Janes
  • 5,524
  • 1
  • 10
  • 22
  • This is a good idea. I think I may be able to make this work; I hadn't considered building any additional metadata into the tag types themselves. I don't see anything these that would preclude a C++03 implementation either. Thanks! – Jason R Nov 06 '17 at 16:11
  • Hmm, after looking at it more, I'm stuck on translating the `std::enable_if_t` on the `tag_matcher` conversion operator to C++03. Default template parameters on functions weren't supported until C++11, so disabling conversion operators via SFINAE [doesn't seem to be possible](https://stackoverflow.com/questions/3076206/enable-if-and-conversion-operator). Do you see any alternate implementation that works around this? – Jason R Nov 08 '17 at 22:01
  • @jasonr you’re right, missed that. Have you tried adding an equivalent tag_matcher converting constructor to the tags instead ? – Massimiliano Janes Nov 09 '17 at 00:02
  • @jasonr ok, tested myself now and it works for me, see edit – Massimiliano Janes Nov 09 '17 at 08:46
1

The easiest way I can think of doing this would severely complicate the definition of your func type. I'm going to use C++14 features here:

#include <type_traits>

template <typename Func, typename Tag1, typename Tag2>
using has_same_tag_overload =
    std::is_same<decltype(Func::fn(Tag1{})), decltype(Func::fn(Tag2{}))>;

struct func
{
    static auto fn(tag_base) {
        return [] { std::cout << "tag_base\n"; };
    }
    static auto fn(tag3) {
        return [] { std::cout << "tag3\n"; };
    }

    template <typename Tag>
    void operator()(Tag tag)
    {
        fn(tag)();
    }
};

The idea is that you have your func struct create the function to call before calling it, having each function be a distinct type. This means that we can simply compare the function type using std::is_same.

This is doable in C++03, but the code gets messier. func would have to return distinct function object types. Instead of decltype, you could possibly use the sizeof trick (have each function object type in func be of a different size, so that you can just compare the sizes.

Justin
  • 24,288
  • 12
  • 92
  • 142
  • Yeah, I thought of something similar, but unfortunately returning `void` was just a simplification for SO. I suppose you could do something like this but add an implicit conversion to the real return type. – Daniel H Nov 05 '17 at 18:26
  • @DanielH handling a return type is extremely easy with this technique: add a return type on the lambdas and the `operator()` – Justin Nov 05 '17 at 18:36