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.