Jeff
Jeff

Reputation: 3525

C++ template class, template member friend function matching rules

I have a templated class with a templated friend function declaration that is not having its signature matched when stated in a more direct, but seemingly equivalent, expression:

link to example on online compiler

#include <type_traits>

template <typename Sig> class Base;
template <typename R, typename ... Args> class Base<R(Args...)> { };
template <typename Sig, typename T> class Derived;
template <typename Sig> struct remove_membership;

template <typename T, typename R, typename ... Args>
class Derived<R(Args...), T> : public Base<R(Args...)> {
  static void bar() { }

  // XXX: why are these two not equivalent, and only the 1st version successful?
  template <typename T2>
  friend auto foo(T2 const &) -> Base<typename
    remove_membership<decltype(&std::remove_reference_t<T2>::operator())>::type> *;
  template <typename T2>
  friend auto foo(T2 const &) -> Base<R(Args...)> *;
};

template <typename F, typename R, typename ... Args>
struct remove_membership<R (F::*)(Args...) const> {
  using type = R(Args...);
};

template <typename T>
auto foo(T const &) -> Base<typename 
remove_membership<decltype(&std::remove_reference_t<T>::operator())>::type> * 
{
  using base_param_t = typename remove_membership<
    decltype(&std::remove_reference_t<T>::operator())>::type;
  Derived<base_param_t, T>::bar();
  return nullptr;
}

int main(int, char **) { foo([](){}); } // XXX blows up if verbose friend decl. removed.

Inside member definitions of Derived<R(Args...), T> (for example, in the body of bar()), the types match, adding to my confusion:

static_assert(std::is_same<Base<R(Args...)>, Base<typename  
  remove_membership<decltype(&std::remove_reference_t<T>::operator())>::type>>::value,
  "signature mismatch");

Are there rules around template class template member function (and friend function) delarations and instantiations that make these preceding declarations distinct in some or all circumstances?

Upvotes: 1

Views: 360

Answers (2)

Jarod42
Jarod42

Reputation: 217970

template <typename T2>
void foo(T2 const &)

template <typename T2>
auto foo(T2 const &)
-> std::enable_if_t<some_traits<T2>::value>;

Are 2 different overloads. Even if both return void (when valid).
2nd overload uses SFINAE.

(and yes, template functions can differ only by return type contrary to regular functions).

Your version is not identical but similar (&std::remove_reference_t<T>::operator() should be valid)

You can use the simpler template friend function:

template <typename T, typename R, typename ... Args>
class Derived<R(Args...), T> : public Base<R(Args...)> {
  static void bar() { }

  template <typename T2>
  friend auto foo(T2 const &) -> Base<R(Args...)>*;
};

template <typename T>
auto foo(T const &) -> Base<void()>* // friend with Derived<void(), U>
{
  using base_param_t = typename remove_membership<
    decltype(&std::remove_reference_t<T>::operator())>::type;
  Derived<base_param_t, T>::bar();
  return nullptr;
}

Demo

but you have then to implement different version of the template foo.

Upvotes: 2

David G
David G

Reputation: 96845

The problem can be reduced to:

template<class T>
struct identity {
    using type=T;
};

class X {
    int bar();
public:
    template<class T>
    friend T foo();
};

template<class T>
typename identity<T>::type foo() { return X{}.bar(); }

int main() {
    foo<int>(); // error: bar is a private member of X
}

Even though we know identity<T>::type is always T, the compiler doesn't know that and would be wrong to assume so. There could be a specialization of identity<T> somewhere later in the code that resolves to some type other than T.

Therefore when the compiler sees the second declaration of foo it won't assume that it is the same friend foo declared before.

Upvotes: 1

Related Questions