不同大小的元组上的 SFINAE 重载

zha*_*nou 5 c++ overloading tuples sfinae

考虑一个包含两种类型 -FirstObjects和 的结构SecondObjects,它们都是std::tuple<>,例如:

struct Objects
{
    using FirstObjects = std::tuple<int, int>;
    using SecondObjects = std::tuple<int>;
}; 
Run Code Online (Sandbox Code Playgroud)

为了方便我们添加以下枚举:

enum class ObjectCategory
{
    FIRST,
    SECOND
};
Run Code Online (Sandbox Code Playgroud)

现在考虑以下在Objects如上所述的类型上模板化的类(唯一的约定是它具有FirstObjectsSecondObjects且它们是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_;
};
Run Code Online (Sandbox Code Playgroud)

简而言之,SomeClass托管两个类型的成员变量,firstObjects_类型为T::FirstObjectssecondObjects_类型为T::SecondObjects,并且具有一个doSomething()函数,该函数使用 SFINAE 重载的getObject()getter 方法,该方法根据所选内容返回包含在firstObjects_或secondObjects_中的第i个对象ObjectCategory

我现在遇到的问题是:

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
}
Run Code Online (Sandbox Code Playgroud)

最后一行让编译器抱怨:

/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> >'
Run Code Online (Sandbox Code Playgroud)

它似乎不喜欢SecondObjects只有一个元素因此std::get<1>(secondObjects_)不起作用的事实?但为什么这件事一开始就发生了,因为我已经打电话doSomethingObjectCategory::FIRST,但没有呢ObjectCategory::SECOND

如何解决这个问题?

------ 编辑 1 ------

@Taekahn指出的解决方案是将getObject()方法更改如下:

    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_);
    }
Run Code Online (Sandbox Code Playgroud)

上面的方法似乎有效,因为当使用关键字而auto不是显式类型时,编译器不会评估/检查已被 SFINAE 删除的方法的返回类型的有效性。但这只是推测 - 非常感谢知识渊博的 C++ 从业者在这里权衡!

------ 编辑 2 ------

当尝试这样做时,上述结果会产生歧义

    object.doSomething<ObjectCategory::SECOND, 0>();
Run Code Online (Sandbox Code Playgroud)

到目前为止我发现的WAR是将第一个getter函数修改为

    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_);
    }
Run Code Online (Sandbox Code Playgroud)

即在末尾添加 SFINAE 以及虚拟模板 arg 以允许重载。