2
votes

I've tried to boil this down to the essentials. I have a variadic template class, Foo, containing a "list" of objects indexed with their types. I use the function bar<U>() to extract the element of that type. I solve this using a variadic template and std::enable_if to only define bar<U>() where T == U. Then I expose all "bar" functions from the base classes with "using".

#include <type_traits>

template<typename... Ts>
class Foo
{
public:
    void bar() {}
};

template<typename T, typename... Ts>
class Foo<T, Ts...> : public Foo<Ts...>
{
public:
    using Foo<Ts...>::bar;

    template<typename U>
    typename std::enable_if<std::is_same<U, T>::value, U >::type
        bar()
    {
        return mObj;
    }
private:
    T mObj;
};

template<typename T>
void bar2()
{
    Foo<int, float, double> list;
    list.bar<T>();
}

int main()
{
    bar2<float>();
    return 0;
}

This works wonderfully, except on Clang and Visual Studio 2015. Tried both MSVC 19.0 and 19.10, and gives the error:

Compiled with  /EHsc /nologo /W4 /c
main.cpp
main.cpp(30): error C2672: 'Foo<int,float,double>::bar': no matching overloaded function found
main.cpp(35): note: see reference to function template instantiation 'void bar2<float>(void)' being compiled
main.cpp(30): error C2770: invalid explicit template argument(s) for 'std::enable_if<std::is_same<U,T>::value,U>::type Foo<int,float,double>::bar(void)'
        with
        [
            T=int
        ]
main.cpp(18): note: see declaration of 'Foo<int,float,double>::bar'

GCC between at least at least 4.7-6.3 compile this fine. I first thought that this might be to some feature from c++11 missing in Visual Studio 2015, but surprisingly this compiles fine in the older Visual Studio 2013 (MSVC 18.0). Clang also fails.

So my question is, is this a shortcoming of these compilers or is there something I'm doing here that isn't allowed?

If I call "bar" with a hard-coded type like list.bar<int>() it compiles on all tested compilers.

2
Reproduced error code can be reduced to that. - Jarod42

2 Answers

1
votes

To use enable_if here, you need to provide an alternate option, for when is_same<U, T>::value == false. Ideally, this could be done by exposing all base class members bar with a using-declaration...

using Foo<Ts...>::template bar;

Unfortunately, this is forbidden by the standard, and it was decided not to rectify this. Therefore, we have to expose them another way. The simplest solution is thus to make a wrapper for Foo<Ts...>::template bar(), as follows:

template<typename T, typename... Ts>
class Foo<T, Ts...> : public Foo<Ts...>
{
public:
    // using Foo<Ts...>::template bar; // Sadly, this is forbidden.

    template<typename U>
    typename std::enable_if<std::is_same<U, T>::value, U >::type
        bar()
    {
        return mObj;
    }

    // Additional wrapper, to compensate for lack of valid syntax.
    template<typename U>
    typename std::enable_if<!std::is_same<U, T>::value, U >::type
        bar()
    {
        return Foo<Ts...>::template bar<U>();
    }

private:
    T mObj;
};

Note, however, that the wrapper cannot call Foo<Ts...>::bar(), due to it returning void. Assuming that it's the generic case, intended to be used when U isn't a member of the pack, there are two ways to rectify this:

  • Modify Foo<Ts...>::bar().

    template<typename... Ts>
    class Foo
    {
    public:
        template<typename T>
        T bar()
        {
            // Return an invalid value.
            return T{-1};
        }
    };
    
  • Provide a third version of Foo<T, Ts...>::bar(), to be used when U isn't a member of T, Ts...; this one returns Foo<Ts...>::bar(). For this, it would be useful to define a trait to detect whether it's in the pack.

    template<typename...>
    struct is_in_pack : std::false_type {};
    
    template<typename U, typename T1, typename... Ts>
    struct is_in_pack<U, T1, Ts...> :
        std::integral_constant<bool,
                               std::is_same<U, T1>::value ||
                               is_in_pack<U, Ts...>::value>
    {};
    

    Then, we just need to use the trait.

    template<typename T, typename... Ts>
    class Foo<T, Ts...> : public Foo<Ts...>
    {
    public:
        // using Foo<Ts...>::template bar; // Sadly, this is forbidden.
    
        template<typename U>
        typename std::enable_if<std::is_same<U, T>::value, U >::type
            bar()
        {
            return mObj;
        }
    
        // Additional wrapper, to compensate for lack of valid syntax.
        // U is a member of <T, Ts...>.
        template<typename U>
        typename std::enable_if<!std::is_same<U, T>::value &&
                                is_in_pack<U, T, Ts...>::value, U >::type
            bar()
        {
            return Foo<Ts...>::template bar<U>();
        }
    
        // Additional wrapper, to compensate for lack of valid syntax.
        // U isn't a member of <T, Ts...>.
        template<typename U>
        typename std::enable_if<!is_in_pack<U, T, Ts...>::value>::type
            bar()
        {
            return Foo<>::bar();
        }
    
    private:
        T mObj;
    };
    

Of these options, I would suggest the latter, as it matches your current code more closely.

Simple test.
Convoluted test.

0
votes

The above should not compile. Your enable_if declaration will mean that bar<U> will only exist if U is an int (i.e. the same as T). Therefore when you are entering list.bar<T>() it is looking for a function that does not exist.

The following will work, as the function will actually exist if you're using int, as that is what you have declared (Foo<int, float, double> list).

int main()
{
    bar2<int /*float*/>();
    return 0;
}

Edit: Just to elaborate a bit more...

I'm not sure what you're trying to accomplish, but maybe you want your class something like this?

template<typename T, typename... Ts>
class Foo<T, Ts...> : public Foo<Ts...>
{
public:
    using Foo<Ts...>::bar;

    template<typename U>
    typename std::enable_if<std::is_same<U, T>::value, U >::type
        bar()
    {
        // Your "int" one will come through here.

        return mObj;
    }

    template <typename U>
    typename std::enable_if<!std::is_same<U, T>::value, void>::type
        bar()
    {
        // Your "float" one will come through here.
    }
private:
    T mObj;
};