2

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.

Jarod42
  • 203,559
  • 14
  • 181
  • 302
Morgan
  • 346
  • 1
  • 7
  • Reproduced error code can be reduced to [that](http://coliru.stacked-crooked.com/a/66742e97d6df3061). – Jarod42 Mar 09 '17 at 01:17

2 Answers2

1

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.

Community
  • 1
  • 1
  • Excellent, that solved it! Actually the original intent was to create a compilation error if called with a type not in the parameter list of the class. – Morgan Mar 10 '17 at 13:32
  • However, my followup question is then; why would the `template` statement be required in the `using` statement in my case? Note that gcc compiled this fine without `template` here. – Morgan Mar 10 '17 at 13:43
  • @Morgan Ah. To cause an error, you could either use the simple version at the top as is, or add something like `static_assert(is_in_pack::value, "Template parameter U isn't a member of pack .");` to the wrapper function if you want to provide more information. – Justin Time - Reinstate Monica Mar 10 '17 at 19:46
  • As to the use of `template` there... if a dependent name isn't a member of the current template instantiation, it isn't considered to be a template name unless either `template` is used as a disambiguator, or it was already established as a template name. Now, the "already established" part is a bit murky; the standard allows compilers to wait until they instantiate the template to finish parsing dependent names, meaning they can wait until they figure out what each member is (in which case, disambiguators `template` & `typename` won't be necessary for members; this is probably what GCC does). – Justin Time - Reinstate Monica Mar 10 '17 at 19:59
  • However, compilers can also parse dependent names immediately, before the template is instantiated; in this case, the compiler won't be able to determine what the name is by itself (for example, for all it knows, there might be a `template<> class Foo { public: int bar; };` waiting to trip it up at instantiation), so it would thus emit a parsing error for `Foo::bar()`, since it doesn't know that `bar` is a template, and `(Foo::bar < U) > ()` is ill-formed. – Justin Time - Reinstate Monica Mar 10 '17 at 20:04
  • In this case, the use of `template` allows the programmer to specify that `bar` is explicitly a template, regardless of `Foo`'s template parameters. This lets the compiler process `Foo::template bar()` as a template name, in which case it's a well-formed call to a template function. The first link in my answer has a good explanation of this; it explains dependent names, `template` as a disambiguator, and `typename` as a disambiguator. – Justin Time - Reinstate Monica Mar 10 '17 at 20:13
0

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;
};
Mitch
  • 1,839
  • 16
  • 23
  • But what about the `using` declaration? That should make other `bar()` declarations visible. – aschepler Mar 09 '17 at 00:31
  • It does make the other `bar()` declaration visible. But, that declaration does not have any template arguments, and you're calling `bar()`. – Mitch Mar 09 '17 at 00:32
  • its only the lowest level base class where bar doesnt have any template arguments. In the case of `Foo`, the base classes `Foo` and `Foo` will have one `bar` function each – Morgan Mar 09 '17 at 00:41
  • So you'll have `Foo::bar` and inherited and exposed from `using`: `Foo::bar` and `Foo::bar` (and also `Foo<>::bar()` from the lowest level base class) – Morgan Mar 09 '17 at 00:44
  • You are `using Foo::bar` which has no template arguments. If you want to expose the version of `bar` that *does* have a template argument from the base class, you'll need a templated using statement. – Mitch Mar 09 '17 at 00:55
  • @Mitch, Na, that's not correct. You can define a templated using statement, that's legal: http://en.cppreference.com/w/cpp/language/type_alias – Morgan Mar 09 '17 at 01:05
  • That's kinda what I just suggested... The fact is that what you have isn't working. Another (probably better) option is to have `using Foo::bar` in a class `Foo : Foo`. – Mitch Mar 09 '17 at 01:12
  • @Mitch: in fact specifying the template parameter of the function is illegal. On gcc you get: `error: a template-id may not appear in a using-declaration` – Morgan Mar 09 '17 at 01:13
  • @Mitch: well yes its working on all gcc-compilers v >=4.7 and Visual Studio 2013 – Morgan Mar 09 '17 at 01:15
  • I'll write a working edit when I get back to a computer. Can't type it on my phone. ☺️ – Mitch Mar 09 '17 at 01:15
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/137612/discussion-between-mitch-and-morgan). – Mitch Mar 09 '17 at 03:25