SFINAE overload on tuples of different sizes

Viewed 100

Consider a struct that contains two types - FirstObjects and SecondObjects, which are both std::tuple<>, e.g. something like:

struct Objects
{
    using FirstObjects = std::tuple<int, int>;
    using SecondObjects = std::tuple<int>;
}; 

For convenience we add the following enum:

enum class ObjectCategory
{
    FIRST,
    SECOND
};

Now consider the following class that is templated on a type such as Objects as described above (the only contract is that it have FirstObjects and SecondObjects and that they be std::tuple):

template <typename T>
class SomeClass
{
public:
    using FirstObjects = typename T::FirstObjects;
    using SecondObjects = typename T::SecondObjects;
    
    template <std::size_t Idx>
    using FirstObject = typename std::tuple_element<Idx, FirstObjects>::type;
    template <std::size_t Idx>
    using SecondObject = typename std::tuple_element<Idx, SecondObjects>::type;        
    
    template <ObjectCategory Category, std::size_t Idx>
    using ObjectType = std::conditional_t<Category == ObjectCategory::FIRST, FirstObject<Idx>, SecondObject<Idx>>;
    
    template <ObjectCategory Category, std::size_t Idx>
    std::enable_if_t<Category == ObjectCategory::FIRST, const ObjectType<Category, Idx>>& getObject()
    {
        return std::get<Idx>(firstObjects_);
    }

    template <ObjectCategory Category, std::size_t Idx>
    std::enable_if_t<Category == ObjectCategory::SECOND, const ObjectType<Category, Idx>>& getObject()
    {
        return std::get<Idx>(secondObjects_);
    }    
    
    template <ObjectCategory Category, std::size_t Idx>
    void doSomething()
    {
        const ObjectType<Category, Idx>& obj = getObject<Category, Idx>();
    }
    
private:
    FirstObjects firstObjects_;
    SecondObjects secondObjects_;
};

In brief, SomeClass hosts two member variables, firstObjects_ of type T::FirstObjects, and secondObjects_ of type T::SecondObjects, and has a doSomething() function which makes use of the SFINAE-overloaded getObject() getter method that returns the i-th object contained in firstObjects_ or in secondObjects_ depending on the ObjectCategory chosen.

The problem I have now is:

int main()
{
    struct Objects
    {
        using FirstObjects = std::tuple<int, int>;
        using SecondObjects = std::tuple<int>;
    };    
    SomeClass<Objects> object{};
    object.doSomething<ObjectCategory::FIRST, 0>(); // <------ works fine
    object.doSomething<ObjectCategory::FIRST, 1>(); // <------ compiler error
}

The last line makes the compiler complain:

/usr/include/c++/4.9/tuple: In instantiation of 'struct std::tuple_element<1ul, std::tuple<int> >':
70:114:   required by substitution of 'template<class T> template<ObjectCategory Category, long unsigned int Idx> using ObjectType = std::conditional_t<(Category == FIRST), typename std::tuple_element<Idx, typename T::FirstObjects>::type, typename std::tuple_element<Idx, typename T::SecondObjects>::type> [with ObjectCategory Category = (ObjectCategory)0; long unsigned int Idx = 1ul; T = main()::Objects]'
87:42:   required from 'void SomeClass<T>::doSomething() [with ObjectCategory Category = (ObjectCategory)0; long unsigned int Idx = 1ul; T = main()::Objects]'
104:50:   required from here
/usr/include/c++/4.9/tuple:682:12: error: invalid use of incomplete type 'struct std::tuple_element<0ul, std::tuple<> >'
     struct tuple_element<__i, tuple<_Head, _Tail...> >
            ^
In file included from /usr/include/c++/4.9/tuple:38:0,
                 from 4:
/usr/include/c++/4.9/utility:85:11: error: declaration of 'struct std::tuple_element<0ul, std::tuple<> >'
     class tuple_element;
           ^
 In substitution of 'template<class T> template<ObjectCategory Category, long unsigned int Idx> using ObjectType = std::conditional_t<(Category == FIRST), typename std::tuple_element<Idx, typename T::FirstObjects>::type, typename std::tuple_element<Idx, typename T::SecondObjects>::type> [with ObjectCategory Category = (ObjectCategory)0; long unsigned int Idx = 1ul; T = main()::Objects]':
87:42:   required from 'void SomeClass<T>::doSomething() [with ObjectCategory Category = (ObjectCategory)0; long unsigned int Idx = 1ul; T = main()::Objects]'
104:50:   required from here
70:114: error: no type named 'type' in 'struct std::tuple_element<1ul, std::tuple<int> >'

It seems like it is not liking the fact that SecondObjects only has one element and therefore std::get<1>(secondObjects_) is not working? But why is this even happening in the first place since I'm calling doSomething on ObjectCategory::FIRST and not ObjectCategory::SECOND?

How can this be solved?

------ EDIT 1 ------

Solution pointed out by @Taekahn is to change the getObject() method as follows:

    template <ObjectCategory Category, std::size_t Idx>
    const auto& getObject()
    {
        return std::get<Idx>(firstObjects_);
    }

    template <ObjectCategory Category, std::size_t Idx, typename = std::enable_if_t<Category == ObjectCategory::SECOND>>
    const auto& getObject()
    {
        return std::get<Idx>(secondObjects_);
    }

It seems like the above works because the compiler does not evaluate / check the validity of the return type of a method that has been SFINAE'd out when one uses the keyword auto instead of the explicit type. But this is only conjecture - would really appreciate a more knowledgeable C++ practitioner to weigh in here!

------ EDIT 2 ------

The above results in ambiguity when trying to do

    object.doSomething<ObjectCategory::SECOND, 0>();

The WAR I've found so far is to modify the first getter function to

    template <ObjectCategory Category, std::size_t Idx, typename = std::enable_if_t<Category == ObjectCategory::FIRST>, bool = 0 /* dummy param */>
    auto& getObject()
    {
        return std::get<Idx>(firstObjects_);
    }

i.e. to add the SFINAE as well as a dummy template arg at the end to allow overloading.

0 Answers
Related