Search code examples
c++type-traitsc++-experimental

Deducing a shared base of two classes in C++


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?


Solution

  • 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.