The title is quoted from this SO answer. It is discussing using SFINAE to detect the existence of a member function with the given signature and points out a failing of the method in the accepted answer when dealing with inherited member functions. In particular, the explanation given is as follows
If you are not already wise to this gotcha, then a look at of the definition of std::shared_ptr<T> in the header will shed light. In that implementation, std::shared_ptr<T> is derived from a base class from which it inherits operator*() const. So the template instantiation SFINAE<U, &U::operator*> that constitutes "finding" the operator for U = std::shared_ptr<T> will not happen, because std::shared_ptr<T> has no operator*() in its own right and template instantiation does not "do inheritance".
This snag does not affect the well-known SFINAE approach, using "The sizeof() Trick", for detecting merely whether T has some member function mf (see e.g. this answer and comments).
Using the terminology from the answer, what is the difference between using T::mf as a template argument to instantiate a type vs having the compiler determine it through a template function argument deduction? What does "template instantiation does not do inheritance" mean? And lastly, why doesn't this affect simply checking for existence of a member, like here?
Minimized example:
struct A {
void a() const;
};
struct B : A {};
template<typename U, void (U::*)() const> struct SFINAE {};
template<typename U> void Test(SFINAE<U, &U::a>*) { }
int main(void)
{
Test<B>(0); // doesn't compile
return 0;
}
Demo.
The problem is that when B::a is inherited from A, the type of &B::a is actually "pointer to member of A" - and, while normally a pointer-to-member-of-base can be implicitly converted to pointer-to-member-of-derived, this conversion doesn't apply for non-type template arguments, per §14.3.2 [temp.arg.nontype]/p5:
The following conversions are performed on each expression used as a
non-type template-argument. If a non-type template-argument cannot
be converted to the type of the corresponding template-parameter
then the program is ill-formed.
[...]
For a non-type template-parameter of type pointer to member function,
if the template-argument is of type std::nullptr_t, the
null member pointer conversion (4.11) is applied; otherwise, no
conversions apply. If the template-argument represents a set of
overloaded member functions, the matching member function is selected
from the set (13.4).
Related
From std::add_pointer
Possible implementation
namespace detail {
template <class T>
struct type_identity { using type = T; }; // or use std::type_identity (since C++20)
template <class T>
auto try_add_pointer(int) -> type_identity<typename std::remove_reference<T>::type*>;
template <class T>
auto try_add_pointer(...) -> type_identity<T>;
} // namespace detail
template <class T>
struct add_pointer : decltype(detail::try_add_pointer<T>(0)) {};
The description for the above (possible) implementation reads:
If T is a reference type, then provides the member typedef type which
is a pointer to the referred type.
Otherwise, if T names an object type, a function type that is not cv-
or ref-qualified, or a (possibly cv-qualified) void type, provides the
member typedef type which is the type T*.
Otherwise (if T is a cv- or ref-qualified function type), provides the
member typedef type which is the type T.
In the (possible) implementation code above, apparently struct add_pointer derives from the type returned by detail::try_add_pointer<T>(0).
What is the logic behind deriving from the type returned by the overload of detail::try_add_pointer<T> taking int argument, to resolve the member typedef type to one of the three possibilities described above? Specifically, how does this address the possibility if T is a cv- or ref-qualified function type?
The key is in understanding how overload resolution for detail::try_add_pointer<T>(0) works. Substituting T into detail::try_add_pointer is meant to produce an overload set that will always contain at least one member (the variable argument overload).
Whether or not the overload taking an int is discarded during overload resolution (SFINAE) is determined by the success of substituting T into typename std::remove_reference<T>::type*. When substitution succeeds, the overload exists, and is a better match in overload resolution for 0 (ellipsis are the worst possible match compared an any other conversion sequence). Either way, whichever overload is picked up in overload resolution, decltype(detail::try_add_pointer<T>(0)) will resolve to something that has a nested ::type member.
So let's go on a case by case analysis:
"If T is a reference type" - Let's mark it T = T2&. Then std::remove_reference<T>::type is T2. The types to which we may form a reference are also the types to which we may form a pointer. So std::remove_reference<T>::type* is well-formed (it's T2*), and the first overload exists. It's picked up in overload resolution. The nested ::type of its return type is T2*.
"Otherwise, if T names an object type, a function type that is not cv- or ref-qualified, or a (possibly cv-qualified) void type" - In this case std::remove_reference<T>::type is simply T. We can form a pointer to any of the types in the previous list, and so std::remove_reference<T>::type* is well formed again (and it's T*). The first overload again exists and is picked up in overload resolution. The nested ::type of its return type is T*.
"Otherwise (if T is a cv- or ref-qualified function type)" - The interesting bit. This speaks of types like void (int) const&. Here again std::remove_reference<T>::type is T. But we are not allowed to form a pointer to T, the base language prohibits it. Therefore std::remove_reference<T>::type* is ill-formed, and for this overload resolution the first overload is disregarded. Left with only the second overload, it's the one picked up by overload resolution. The nested ::type of its return type is T.
The inheritance is just a way of using the type_identity wrapper (already needed to form valid return types) to define the trait with minimal effort. The overload trick relies on the fact that std::remove_reference is the identity for non-reference types and that (having dealt with references) std::add_pointer<T> is T* except when there is no such type at all. In that case, SFINAE kills off the int overload and the ... is selected (where just T is produced).
template<auto>
struct S {};
template<S>
struct T {};
using T0 = T<S<0>{}>; // compiles
template<S s>
using T1 = T<s>; // fails
T0 compiles with GCC 9.1 but T1 does not:
error: class template argument deduction failed
error: no matching function for call to 'S(S<...auto...>)'
Is this expected behavior in C++2a?
EDIT:
The compilation failure appears to occur whenever the type is deduced, not specifically when a placeholder for a deduced class type is used:
template<auto s>
using T1 = T<s>; // also fails
T0 should be ill-formed because S<0> does not have strong structural equality.
T1 should be ill-formed, no diagnostic required, because as it stands there is no specialization of T1 that would be valid (because there is no specialization of S that has strong structural equality).
This is because the requirement on class types as non-type template parameters is, from [temp.param]/4:
A non-type template-parameter shall have one of the following (optionally cv-qualified) types:
a literal type that has strong structural equality ([class.compare.default]),
an lvalue reference type,
a type that contains a placeholder type ([dcl.spec.auto]), or
a placeholder for a deduced class type ([dcl.type.class.deduct]).
template<S>
struct T {};
falls into the last case. But when we try T<S<0>{}>, and we deduce the placeholder S as S<0> from CTAD, we hit [temp.arg.nontype]/1:
If a deduced parameter type is not permitted for a template-parameter declaration ([temp.param]), the program is ill-formed.
Which takes us back to the original reference and now we require something called "strong structural equality." This is defined in [class.compare.default]/4 as:
A type C has strong structural equality if, given a glvalue x of type const C, either:
C is a non-class type and x <=> x is a valid expression of type std::strong_ordering or std::strong_equality, or
C is a class type where all of the following hold:
All of C's base class subobjects and non-static data members have strong structural equality.
C has no mutable or volatile non-static data members.
At the end of the definition of C, overload resolution performed for the expression x == x succeeds and finds either a friend or public member == operator that is defined as defaulted in the definition of C.
In our case, we're a class type, but while we all our subobjects have strong structural equality (trivially as we have no subobjects), we do not have the appropriate operator==. To fix that:
template <auto>
struct S {
friend bool operator==(S const&, S const&) = default;
};
And now everything should compile (but doesn't because gcc doesn't completely support this yet, and indeed doesn't even let you declare the required defaulted operator==).
What is wrong with this code? I thought I could convert due to this answer:
Is it safe to "upcast" a method pointer and use it with base class pointer?
struct B
{
void f(){}
};
struct D : B
{
virtual ~D(){}
};
template <typename FP, FP fp>
void g()
{
}
int main()
{
g<void (D::*)(), &B::f>();
return 0;
}
Error:
t.cpp:18:27: error: could not convert template argument '&B::f' to 'void (D::*)()'
g<void (D::*)(), &B::f>();
This doesn't work either:
g<void (D::*)(), static_cast<void (D::*)()>(&B::f)>();
This is disallowed by the standard (C++11, [temp.arg.nontype]§5):
The following conversions are performed on each expression used as a non-type template-argument. If a non-type template-argument cannot be converted to the type of the corresponding template-parameter then the program is ill-formed.
...
For a non-type template-parameter of type pointer to member function, if the template-argument is of type std::nullptr_t, the null member pointer conversion (4.11) is applied; otherwise, no conversions apply. If the template-argument represents a set of overloaded member functions, the matching member function is selected from the set (13.4).
(Emphasis mine)
Casts are not allowed either, because of [temp.arg.nontype]§1:
A template-argument for a non-type, non-template template-parameter shall be one of:
...
a pointer to member expressed as described in 5.3.1.
Where 5.3.1§4 reads:
A pointer to member is only formed when an explicit & is used and its operand is a qualified-id not enclosed in parentheses.
This combines to say that a cast experssion is not allowed as a non-type template argument.
So, while such conversions are possible at runtime, it seems there's no way to use them as template arguments.
What is wrong with this code? I thought I could convert due to this answer:
Is it safe to "upcast" a method pointer and use it with base class pointer?
struct B
{
void f(){}
};
struct D : B
{
virtual ~D(){}
};
template <typename FP, FP fp>
void g()
{
}
int main()
{
g<void (D::*)(), &B::f>();
return 0;
}
Error:
t.cpp:18:27: error: could not convert template argument '&B::f' to 'void (D::*)()'
g<void (D::*)(), &B::f>();
This doesn't work either:
g<void (D::*)(), static_cast<void (D::*)()>(&B::f)>();
This is disallowed by the standard (C++11, [temp.arg.nontype]§5):
The following conversions are performed on each expression used as a non-type template-argument. If a non-type template-argument cannot be converted to the type of the corresponding template-parameter then the program is ill-formed.
...
For a non-type template-parameter of type pointer to member function, if the template-argument is of type std::nullptr_t, the null member pointer conversion (4.11) is applied; otherwise, no conversions apply. If the template-argument represents a set of overloaded member functions, the matching member function is selected from the set (13.4).
(Emphasis mine)
Casts are not allowed either, because of [temp.arg.nontype]§1:
A template-argument for a non-type, non-template template-parameter shall be one of:
...
a pointer to member expressed as described in 5.3.1.
Where 5.3.1§4 reads:
A pointer to member is only formed when an explicit & is used and its operand is a qualified-id not enclosed in parentheses.
This combines to say that a cast experssion is not allowed as a non-type template argument.
So, while such conversions are possible at runtime, it seems there's no way to use them as template arguments.
I'm trying to do this template instantiation but it's not working. I'm getting the errors:
prog.cpp:7:15: error: template-id 'f<const A&, A()>' for 'void f()' does not match any template declaration
template <class T, T> void f() {}
struct A {};
template void f<const A &, A()>();
int main() {}
This is weird, because when I do it in main it works:
int main() {
const A &a = A(); // no error
}
So why doesn't it work in the template line?
Possible duplicate of Non-type template parameters
These are the rules of template non type parameters
A non-type template-parameter shall have one of the following (optionally cv-qualified) types:
integral or enumeration type,
pointer to object or pointer to function,
lvalue reference to object or lvalue reference to function,
pointer to member,
std::nullptr_t.
What you are passing is an RValue (temporary object, etc which cannot be assigned to), which does not fall under any of these possibilities.
edit:
It appears that it is infact being interpreted as a function type, but your template signature expects a non type parameter of type A (exactly a const A&)
A template argument cannot be a temporary object. Only primitive types which can reasonably be compared for exact equality may be template non-type arguments. This includes
integers,
enumerators, and
pointers to objects with extern linkage.
But
floating-point numbers aren't allowed because they can be very close yet not equal
static objects might have the same name but different locations in different files, which would make the template-id confusingly resolve to different instantiations with the same name in different files
same goes for string literals
temporary objects don't have consistent addresses so you can't pass a pointer to one
the value of a temporary object as you passed, which can't even be tested for equality, would never let the language match one template instantiation to another!
(As Pubby notes, A() is actually interpreted as the type of a function with no parameters returning A. So the compiler is just failing to find a template declaration taking two type parameters.)