30

Intro

Let's suppose that we have a linear hierarchy of types like the following:

toy linear hierarchy

Then what I want is a mechanism to return the lowest common ancestor out of an arbitrary number of types in that lineage.

Attempted Code

template<typename...Ts>
struct LCA;

template<typename T1, typename T2, typename...Ts>
struct LCA<T1, T2, Ts...>
{
    using base = typename std::conditional
    <
        std::is_base_of<T1, T2>::value, T1,
        typename std::conditional <
            std::is_base_of<T2, T1>::value, T2, void
        >::type
    >::type;

    using type = typename LCA<base, Ts...>::type;
};

template<typename T>
struct LCA<T>
{
    using type = T;
};

Live Demo

Use Case

My use case is rather typical: In making some iterator tools I want to extract the "most restrictive" iterator type, so since there's (kind of) a linear hierarchy in iterators I should to able to ascent the hierarchy as much as it's needed:

LCA<Bidirectional, RandomAccess, RandomAccess> -> Bidirectional
LCA<RandomAccess, Input, Forward>              -> Input

Questions

  1. Is there a more concise / idiomatic way of handling of the error case where two or more types are strangers to the hierarchy? The current approach is to return void which hopefully will give failure in most contexts where the type is actually used.

  2. Is the use of an extra base member in the first specialization problematic? Should I extract that functionality in a separate class and use it inline in type to maintain uniformity?

  3. Is there an algorithm that would reduce the number of instantiations? Is there a better way than pairwise comparisons, so that the complexity of the algorithm can be reduced?

  4. Can anyone scale to non linear hierarchies and query by depth a hierarchy tree? What would be a good "tie breaker" in that case (for types in the same level)?

Peter O.
  • 32,158
  • 14
  • 82
  • 96
Nikos Athanasiou
  • 29,616
  • 15
  • 87
  • 153
  • *"Is there a more concise / idiomatic way of handling of the error case"* Depends on what you want. For a hard error, add a `static_assert`. Otherwise, either use `void` or a custom error (tag) type. – dyp Jul 01 '14 at 20:52
  • 17
    Wouldn't simply using `typename std::remove_pointer< typename std::common_type< typename std::add_pointer::type... >::type >::type` work for you? [**Live example**](http://coliru.stacked-crooked.com/a/9a56d9645619e42e) – Daniel Frey Jul 01 '14 at 22:12
  • On a set of types of the same linear hierarchy, `is_base_of` defines a strict ordering. Therefore, you can use the "usual" algorithms to find the maximum or sort the types. These might allow a reduction of instantiations. – dyp Sep 08 '14 at 14:55
  • [Type max, live example](http://coliru.stacked-crooked.com/a/0db134596acacca2). Is your live demo output correct? From the question description, I thought you wanted to have the least derived type, not the most derived type in the list. – dyp Sep 08 '14 at 15:05
  • 1
    Are you trying to reimplement std::common_type? :) EDIT: See this question for an explained implementation:
    [std::common_type implementation](http://stackoverflow.com/questions/12290046/stdcommon-type-implementation)
    – pdw Jul 06 '14 at 08:46
  • For 1, the idiomatic way would be to use SFINAE to make it so that there is no type variable at all. For 2, you want uniformity in usage, not code, so I think it is acceptable. For 3, this is all compile-time calculation and n will always be small so I wouldn't worry about it. There is some stuff you can do with the DAG but not to much. 4. Maybe...how do you want to handle virtual inheritance? If A and B are in the same hierarchy but only share a parent, what do you want to do? – IdeaHat Sep 09 '14 at 17:18
  • Can OP (1) explain why @DanielFrey's implementation is insufficient, and (2) offer an example of a use case? (edit: I just saw the bounty comment, okay, I take it the interest is an academic one? As in somehow in some template madness somewhere you've got a bag of types (that you know to be all part of a hierarchy chain) and you want to reduce the set to the one type that is the highest-up) -- it seems to me like it makes much more sense to examine and learn the implementation of `std::common_type` and use it. – Steven Lu Sep 10 '14 at 17:52
  • You should add the bounty text to your question, since it will disappear without a trace after the bounty ends. – Peter O. Sep 15 '14 at 07:11

2 Answers2

14

1. Technical aspect

I'd use derivation, because this is cleaner than type definitions. Here is some example code:

#include <iostream>
#include <typeinfo>
#include <type_traits>

struct Grandma {};
struct Mom : Grandma {};
struct Daughter : Mom {};
struct Son : Mom {};
struct Grandchild : Son {};

struct Stranger {};

namespace detail
{
    struct TypeIsNotPartOfTheHierarchy {};

    template<typename T>
    struct TypeWrapper
    {
        static_assert(!std::is_same<TypeIsNotPartOfTheHierarchy, T>::value,
            "using types of different type hierarchies.");

        using type = T;
    };
}

template<typename... Ts>
struct LCA;

template<typename T>
struct LCA<T>: detail::TypeWrapper<T>
{};

template<typename T1, typename T2>
struct LCA<T1, T2>:
    std::conditional
    <
        std::is_base_of<T1, T2>::value,
        detail::TypeWrapper<T1>,
        typename std::conditional
        <
            std::is_base_of<T2, T1>::value,
            detail::TypeWrapper<T2>,
            detail::TypeWrapper<detail::TypeIsNotPartOfTheHierarchy>
        >::type
    >::type
{};

template<typename T1, typename... Ts>
struct LCA<T1, Ts...>: LCA<T1, typename LCA<Ts...>::type>
{};

int main()
{
    std::cout << typeid(LCA<Son, Mom, Grandchild, Grandma, Son, Son>::type).name() << std::endl;
    std::cout << typeid(LCA<Son>::type).name() << std::endl;

    // error because Daughter and Son are siblings.
    // std::cout << typeid(LCA<Son, Daughter, Son>::type).name() << std::endl;

    // error because Son is not related to the Stranger.
    // std::cout << typeid(LCA<Son, Stranger, Son>::type).name() << std::endl;

    return 0;
}

Technically you could use std::enable_if instead of std::condition, but using std::enable_if would mean, that you have to derive from the if-true, if-false and if-types-not-compatible case. Using std::condition is IMHO more readable. The type has to be wrapped once more to have a type, that could be enabled by the conditions and then deliver a typedef for using it outside.

In order get a compilation error, statically asserting it would give you a nice message instead of difficult template errors in the compiler output. Then you are free to use the void to signalize an error. I would recommend to use an extra type to name this error. This also improves readability.

2. Base type

I think the base member should be hidden, because else you reveal more than needed to the users and this may confuse them. The use of type derivation solves this issue.

3. Complexity

I think, that it is not possible to improve the complexity O(n). You have to check each type at least once, if it could be the LCA type. So every type is at least once part of a comparison.

4. Other hierarchies (the beautiful part)

The implementation above (as yours too) makes no point on other hierarchies than linear ones (e.g. LCA<Daughter, Grandma, Son> will return Grandma while LCA<Grandma, Daughter, Son>::type will result in an error, because only the neighbour types are compared).

However there are two types of "branching inheritance" in C++ possible (and mixing it of course):

Tree with multiple roots:

struct Dad {};
struct Mom {};
struct Son: Dad, Mom {};

For several cases the LCA is undefined (e.g. LCA<Mom, Dad>::type I guess, that Mom and Dad do not share the same parents). So I would recommend to drop this case.

Tree with one root:

struct Mom {};
struct Son: Mom {};
struct Daughter: Mom {};

I would recommend, that the algorithm returns only a type, if there is one type in the list, to which all types could be casted into (e.g. LCA<Son, Daughter>::type has no LCA, because I hope that they are siblings). So we search that type in the list that is a base type of all others.

Because only neighbour types are compared to each other above, the comparison has to be extended to compare every type with each other (sadly this is O(n^2)). So the basic idea is to check for every type, if it is a common ancestor for all other types. This is only the case for the LCA. BTW: Solving it that way has another advantage, because you will get an error in a "multiple roots"-scenario, but the correct result, if the multiple roots are joining again in a common root (that is part of the list).

We need first of all a functionality, that determines whether one type is a base type of all other or not:

template<typename StillCommonAncestor, typename TypeToCheck, typename... Ts>
struct IsCommonAncestor;

template<typename StillCommonAncestor, typename TypeToCheck>
struct IsCommonAncestor<StillCommonAncestor, TypeToCheck>
{
    static constexpr bool value = StillCommonAncestor::value;
};

template<typename StillCommonAncestor, typename TypeToCheck, typename T1, typename... Ts>
struct IsCommonAncestor<StillCommonAncestor, TypeToCheck, T1, Ts...>:
    IsCommonAncestor
    <
        std::integral_constant
        <
            bool,
            std::conditional
            <
                std::is_base_of<TypeToCheck, T1>::value,
                std::true_type,
                std::false_type
            >::type::value && StillCommonAncestor::value
        >,
        TypeToCheck,
        Ts...
    >
{};

To check whether a type is the common ancestor of all others, simply use IsCommonAncestor<std::true_type, Mom, Grandchild, Daughter, Son>::value (which is here true, while IsCommonAncestor<std::true_type, Grandchild, Grandchild, Daughter, Son>::value is false). Note that, the value is also false, if one type is not part of the type hierarchy.

Then some "facility" is needed, to iterate through the types and catch the only one, for which IsCommonAncestor<...>::value is true:

template<typename Pack, typename... Ts>
struct LCA;

template<typename... PackParams, typename T1>
struct LCA<std::tuple<PackParams...>, T1>:
    std::conditional
    <
        IsCommonAncestor<std::true_type, T1, PackParams...>::value,
        TypeWrapper<T1>,
        TypeWrapper<TypeIsNotPartOfTheHierarchy>
    >::type
{};

template<typename... PackParams, typename T1, typename... Ts>
struct LCA<std::tuple<PackParams...>, T1, Ts...>:
    std::conditional
    <
        IsCommonAncestor<std::true_type, T1, PackParams...>::value,
        TypeWrapper<T1>,
        LCA<std::tuple<PackParams...>, Ts...>
    >::type
{};

The LCA compares every element with the whole template parameter pack. The first that is the base type of all is used. If the last is also no base type of all others, LCA derives again from TypeWrapper<TypeIsNotPartOfTheHierarchy>, which will raise the typical static assertion.

This one is very inconvenient. A wrapper will fix this:

template<typename... Ts>
struct LCA: detail::LCA<std::tuple<Ts...>, Ts...>
{};

Complete code for the LCA of a tree is available here: http://ideone.com/pYEPYl

Stefan Weiser
  • 2,264
  • 16
  • 25
3
  1. std::enable_if results in a compilation error, if the first template parameter is false. Another alternative to falling back to defining a void type is to result in a compilation error.

  2. I think that the template will be neater if explicit inheritance is used, as a means of resolving to the correct type, see below.

  3. I don't see how the complexity could be reduced below linear complexity. Somehow, in some way, you have to iterate over all the template parameter types, in order to select one of them.

  4. std::is_base_of doesn't really give you an indication of how deep the subclass goes below the superclass, just that one is a subclass of the other. Additionally, with multiple inheritance, a given subclass could occur at different levels below the superclass, so the semantics there are a bit muddy.

Anyway, I think that using a separate class to perform the pairwise type comparison, and using inheritance, looks cleaner:

template<typename T1, typename T2, bool is_t1_base, bool is_t2_base>
class which_one_is_base;

// If T1 and T2 are the same, dontcare will be true.

template<typename T1, typename T2, bool dontcare>
class which_one_is_base<T1, T2, true, dontcare> {

public:
    typedef T1 type;
};

template<typename T1, typename T2>
class which_one_is_base<T1, T2, false, true> {

public:
    typedef T2 type;
};

template<typename T1, typename T2>
class select_base : public which_one_is_base<T1, T2,
                         std::is_base_of<T1, T2>::value,
                         std::is_base_of<T2, T1>::value>
{
};

//////////////////////////////////////////////////////////////////////

template<typename ...Ts> class LCA;

template<typename T1> class LCA<T1> {

public:
    typedef T1 type;
};

template<typename T1, typename T2, typename ...Ts>
class LCA<T1, T2, Ts...> :
    public LCA< typename select_base<T1, T2>::type, Ts...> {
};
Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148