Adrian
Adrian

Reputation: 10911

Can't use base class template member function

For some reason, I can see the top most template<typename T> X<...>::fn(T&&), but not the base class versions. Importing them using the using keyword doesn't work. As I understand it:

In Class definition

... If the derived class already has a member with the same name, parameter list, and qualifications, the derived class member hides or overrides (doesn't conflict with) the member that is introduced from the base class.

Sample code:

#include <iostream>
#include <type_traits>

#define ENABLE_IF(...) std::enable_if_t<__VA_ARGS__, int> = 0

struct dummy {};

template<typename T>
struct always_false : std::false_type {};

template<typename...Ts>
struct X;

template<typename Tx, typename T, typename...Ts>
struct X<Tx, T, Ts...> : X<Tx, Ts...>
{
    using X<Tx, Ts...>::fn;

    template<typename R, ENABLE_IF(std::is_same<T, R>::value)>
    auto fn(R&& x)
    {
        return x;
    }
};

template<typename Tx>
struct X<Tx>
{
    template<typename R>
    auto fn(R&& x)
    {
        static_assert(always_false<R>::value, "Invalid type");
    }
};

int main()
{
    X<dummy, int, float> x;
    std::cout << x.fn(1) << std::endl;
    std::cout << x.fn(1.f) << std::endl;
    std::cout << "Hello, world!\n";
}

I've tried this on g++, clang and VC++ and they all have various errors, (ambiguous call, member function disabled and could not deduce). It's interesting to note that g++ is failing on the call to X<dummy, int, float>::fn(int&&), whereas clang and VC++ are failing on calling X<dummy, int, float>::fn(float&&).

As I understand it, the compiler should ignore the absolute base class member function template<typename R> R X<dummy>::fn(R&&) when calling X<dummy, int, float>::fn(float&&), because that template should resolve to float X<dummy>::fn(float&&) which is an exact match to derived member function float X<dummy, float>::fn(float&&) requiring the derived one to be called without ambiguity.

What am I doing wrong? What am I not understanding?

Edit

To paraphrase T.C.'s answer so far, "this is what the spec says", which I would say is not the correct interpretation. The two points given there are in conflict with each other. If they are an equally good match (point 1), then only the most derived function signature should be visible (point 2).

Anyway, if the problem is a spec problem, then it should go away if I were to disable the possibility of a matching overload which would result in an ambiguity. Thus the following should work:

#include <iostream>
#include <type_traits>

#define ENABLE_IF(...) std::enable_if_t<__VA_ARGS__, int> = 0


template<typename T>
struct always_false : std::false_type {};


template<typename...Ts>
struct list {};


template<typename...Ts>
struct is_one_of;

template<template <typename...> class TT, typename T, typename T1, typename...Ts>
struct is_one_of<T, TT<T1, Ts...>> : is_one_of<T, TT<Ts...>> {};

template<template <typename...> class TT, typename T, typename...Ts>
struct is_one_of<T, TT<T, Ts...>> : std::true_type {};

template<template <typename...> class TT, typename T>
struct is_one_of<T, TT<>> : std::false_type {};


template<typename...Ts>
struct X;

template<typename L, typename T, typename...Ts>
struct X<L, T, Ts...> : X<L, Ts...>
{
    using X<L, Ts...>::fn;

    template<typename R, ENABLE_IF(std::is_same<T, R>::value)>
    constexpr auto fn(R&& x) const
    {
        return x;
    }
};

template<typename L>
struct X<L>
{
    template<typename R, ENABLE_IF(!is_one_of<R, L>::value)>
    constexpr auto fn(R&& x) const
    {
        static_assert(always_false<R>::value, "Type R didn't match");
    }
};


template<typename...Ts>
struct XX : X<list<Ts...>, Ts...> {};

int main()
{
    XX<int, float> x;
    std::cout << x.fn(1) << std::endl;
    std::cout << x.fn(2.f) << std::endl;
}

It does work under g++, but has the same problems under clang and VC++. So, is g++ the only one that is conforming here and the rest are defective?

Upvotes: 0

Views: 873

Answers (1)

T.C.
T.C.

Reputation: 137394

Two things:

  1. Your baseline catch-all version of fn is an equally good match as the other fn varieties; therefore at best you'll get an ambiguous overload error.

  2. Hiding for using-declarations does not consider the full signature (which for function templates would include the template parameter list). It only considers 1) name, 2) (function) parameter-type-list, 3) cv-qualification, and 4) ref-qualifier (if any). If all four match, the base class function template is hidden and not brought in by the using-declaration. Notably, the template parameter list is not considered. In your case, the only thing different between the various fns is the template parameter list; they all have the same name, same parameter-type-list, same cv-qualification, and same ref-qualifier (or lack thereof). Therefore, the most derived fn will hide all fns from the base classes.

    GCC appears to not implement this part to spec and consider the template parameter list when deciding on hiding.

    One possible fix for this part is to move the enable_if to a function parameter, which is considered by the hiding check.


The way overload resolution in C++ works is:

  1. Name lookup to build a set of candidate functions and function templates.
  2. For each candidate function template, perform template argument deduction. If deduction fails, remove it from the overload set. If deduction succeeds, replace the candidate function template with the deduced specialization.
  3. Eliminate non-viable candidates from the set of candidates.
  4. If the set is empty, error. Otherwise, find the best viable function in the candidates.

Hiding operates at the first step: if a declaration is hidden, it is not found by name lookup, and therefore it is not in the initial set of candidates and will not be considered by overload resolution under any circumstance, regardless of what happens in step 2, 3, or 4. A hidden declaration effectively doesn't exist for the purposes of overload resolution.

So, in your case, the base class fns are all hidden. What does it mean? It means that the only candidate found by name lookup is the int one from the most derived class, nothing else. If template argument deduction and substitution succeed, that function template will be called. If they fail (as in the x.fn(2.f) case), then there is no viable candidate left, and you get an error.

Upvotes: 3

Related Questions