Home > OS >  Deducing a shared base of two classes in C
Deducing a shared base of two classes in C

Time:05-26

I am almost certain that what I'm looking for cannot be done without reflection, which is not yet in the language. But occasionally I'm getting surprised with exceptional answers in SO, so let's try.

Is it possible to deduce the "common_base" of two types that have a common shared base class, so the following would be possible (pseudo code! -- there is no common_base_t in the language, this is the magic I'm trying to achieve):

template<typename T1, typename T2>
const common_base_t<T1, T2>& foo(const T1& a1, const T2& a2) {
    if(a1 < a2) return a1;
    return a2;
}

Note that a1 and a2 above do not share a common_type, just by being siblings (sharing the same base) thus we cannot use the ternary operator.

Note also that changing the above return type to const auto& doesn't do the trick (it would not compile: inconsistent deduction for auto return type).

Here is a the naïve implementation, requiring the caller to state the expected return type:

template<typename R>
const R& foo(const auto& a1, const auto& a2) {
    if(a1 < a2) return a1;
    return a2;
}

Then we can call it with:

MyString1 s1 = "hello"; // MyString1 derives from std::string
MyString2 s2 = "world"; // MyString2 also derives from std::string
std::cout << foo<std::string>(s1, s2); // ok we return const lvalue ref
                                       // pointing to one of the above objects

There are many reasons for why this probably cannot be achieved without providing the expected return value. But maybe it could be achieved somehow?

CodePudding user response:

The standard library's std::common_reference<> is tantalizingly close to what you want, and arguably what your foo() function should be using, as it clearly expresses the desired semantics:

template<typename T1, typename T2>
std::common_reference_t<const T1&, const T2&> foo(const T1& a1, const T2& a2) {
    if(a1 < a2) return a1;
    return a2;
}

Unfortunately, it doesn't work out of the box for this specific use-case, as it cannot detect common bases unless one of the types derives from the other.

However, you can give it a hint by specializing std::common_type. Like so:

namespace std {
    template<>
    struct common_type<MyString1, MyString2> {
        using type = std::string;
    };
}

And it will "just work". You can see it in action here: https://gcc.godbolt.org/z/e3PrecPac.

Edit: It's worth mentioning that, depending on your circumstances, you could also create a general purpose specialization of std::common_type for all types that derive from a given base:

struct SomeBase {};

namespace std {
    template<std::derived_from<SomeBase> T1, std::derived_from<SomeBase> T2>
    struct common_type<T1, T2> {
        using type = SomeBase;
    };
}

However, I would thread lightly with this. It's a potentially very broad and wide-ranging partial specialization. It could easily lead to ambiguities, especially if done more than once.

CodePudding user response:

I can think of three approaches.

#1 is waiting for/using reflection.

#2 is using std::tr2::direct_bases and std::tr2::bases from way back. This is going to be a pain if you want to be able to handle "the common base is not the direct base, nor is it unique".

Doing this requires a metaprogramming library, and you end up with something like:

template<class Lhs, class Rhs>
struct common_base< Lhs, Rhs,
  std::enable_if_t< always_true< extract_if_unique_t<common_direct_bases_t<Lhs, Rhs>> > >
> {
  using type = extract_if_unique_t<common_direct_bases_t<Lhs, Rhs>>;
};

and it gets complex, with writing a bunch of metaprogramming boilerplate.

#3 is providing a canonical list of bases you are looking for as common bases, and searching for them as possible bases of your types. Often this is a good idea, as it means uninvolved implementation detail types don't derail you (the open-closed principle).

For the last one, I'd just do a is_base_of filter on the list of canonical bases in order for both types, then grab the first one in both lists.

template<template<class...>class Op, class List>
struct filter;
template<class Lhs, class Rhs>
struct intersect_lists;
template<class List>
struct front;

template<class...>
struct types {};

using canonical_bases = types<A,B,C,D>; // in order

template<class Derived>
struct BaseTest {
  template<class Base>
  using result = std::is_base_of_t< Base, Derived >;
};

template<class Lhs, class Rhs>
using canonical_common_base_of = 
  front_t< intersect_lists_t<
    filter_t<BaseTest<Lhs>::template result, canonical_bases>,
    filter_t<BaseTest<Rhs>::template result, canonical_bases>
  >>;

with another few dozen lines of metaprogramming (or use an existing metaprogramming to reproduce something equivalent).

  • Related