The accepted answer to this question of compiletime member-function
introspection, although it is justly popular, has a snag which can be observed
in the following program:
#include <type_traits>
#include <iostream>
#include <memory>
/* Here we apply the accepted answer's technique to probe for the
the existence of `E T::operator*() const`
*/
template<typename T, typename E>
struct has_const_reference_op
{
template<typename U, E (U::*)() const> struct SFINAE {};
template<typename U> static char Test(SFINAE<U, &U::operator*>*);
template<typename U> static int Test(...);
static const bool value = sizeof(Test<T>(0)) == sizeof(char);
};
using namespace std;
/* Here we test the `std::` smart pointer templates, including the
deprecated `auto_ptr<T>`, to determine in each case whether
T = (the template instantiated for `int`) provides
`int & T::operator*() const` - which all of them in fact do.
*/
int main(void)
{
cout << has_const_reference_op<auto_ptr<int>,int &>::value;
cout << has_const_reference_op<unique_ptr<int>,int &>::value;
cout << has_const_reference_op<shared_ptr<int>,int &>::value << endl;
return 0;
}
Built with GCC 4.6.3, the program outputs 110
- informing us that
T = std::shared_ptr<int>
does not provide int & T::operator*() const
.
If you are not already wise to this gotcha, then a look at of the definition of
std::shared_ptr<T>
in the header <memory>
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). But
establishing that T::mf
exists is often (usually?) not good enough: you may
also need to establish that it has a desired signature. That is where the
illustrated technique scores. The pointerized variant of the desired signature
is inscribed in a parameter of a template type that must be satisfied by
&T::mf
for the SFINAE probe to succeed. But this template instantiating
technique gives the wrong answer when T::mf
is inherited.
A safe SFINAE technique for compiletime introspection of T::mf
must avoid the
use of &T::mf
within a template argument to instantiate a type upon which SFINAE
function template resolution depends. Instead, SFINAE template function
resolution can depend only upon exactly pertinent type declarations used
as argument types of the overloaded SFINAE probe function.
By way of an answer to the question that abides by this constraint I'll
illustrate for compiletime detection of E T::operator*() const
, for
arbitrary T
and E
. The same pattern will apply mutatis mutandis
to probe for any other member method signature.
#include <type_traits>
/*! The template `has_const_reference_op<T,E>` exports a
boolean constant `value that is true iff `T` provides
`E T::operator*() const`
*/
template< typename T, typename E>
struct has_const_reference_op
{
/* SFINAE operator-has-correct-sig :) */
template<typename A>
static std::true_type test(E (A::*)() const) {
return std::true_type();
}
/* SFINAE operator-exists :) */
template <typename A>
static decltype(test(&A::operator*))
test(decltype(&A::operator*),void *) {
/* Operator exists. What about sig? */
typedef decltype(test(&A::operator*)) return_type;
return return_type();
}
/* SFINAE game over :( */
template<typename A>
static std::false_type test(...) {
return std::false_type();
}
/* This will be either `std::true_type` or `std::false_type` */
typedef decltype(test<T>(0,0)) type;
static const bool value = type::value; /* Which is it? */
};
In this solution, the overloaded SFINAE probe function test()
is "invoked
recursively". (Of course it isn't actually invoked at all; it merely has
the return types of hypothetical invocations resolved by the compiler.)
We need to probe for at least one and at most two points of information:
- Does
T::operator*()
exist at all? If not, we're done.
- Given that
T::operator*()
exists, is its signature
E T::operator*() const
?
We get the answers by evaluating the return type of a single call
to test(0,0)
. That's done by:
typedef decltype(test<T>(0,0)) type;
This call might be resolved to the /* SFINAE operator-exists :) */
overload
of test()
, or it might resolve to the /* SFINAE game over :( */
overload.
It can't resolve to the /* SFINAE operator-has-correct-sig :) */
overload,
because that one expects just one argument and we are passing two.
Why are we passing two? Simply to force the resolution to exclude
/* SFINAE operator-has-correct-sig :) */
. The second argument has no other signifance.
This call to test(0,0)
will resolve to /* SFINAE operator-exists :) */
just
in case the first argument 0 satifies the first parameter type of that overload,
which is decltype(&A::operator*)
, with A = T
. 0 will satisfy that type
just in case T::operator*
exists.
Let's suppose the compiler say's Yes to that. Then it's going with
/* SFINAE operator-exists :) */
and it needs to determine the return type of
the function call, which in that case is decltype(test(&A::operator*))
-
the return type of yet another call to test()
.
This time, we're passing just one argument, &A::operator*
, which we now
know exists, or we wouldn't be here. A call to test(&A::operator*)
might
resolve either to /* SFINAE operator-has-correct-sig :) */
or again to
might resolve to /* SFINAE game over :( */
. The call will match
/* SFINAE operator-has-correct-sig :) */
just in case &A::operator*
satisfies
the single parameter type of that overload, which is E (A::*)() const
,
with A = T
.
The compiler will say Yes here if T::operator*
has that desired signature,
and then again has to evaluate the return type of the overload. No more
"recursions" now: it is std::true_type
.
If the compiler does not choose /* SFINAE operator-exists :) */
for the
call test(0,0)
or does not choose /* SFINAE operator-has-correct-sig :) */
for the call test(&A::operator*)
, then in either case it goes with
/* SFINAE game over :( */
and the final return type is std::false_type
.
Here is a test program that shows the template producing the expected
answers in varied sample of cases (GCC 4.6.3 again).
// To test
struct empty{};
// To test
struct int_ref
{
int & operator*() const {
return *_pint;
}
int & foo() const {
return *_pint;
}
int * _pint;
};
// To test
struct sub_int_ref : int_ref{};
// To test
template<typename E>
struct ee_ref
{
E & operator*() {
return *_pe;
}
E & foo() const {
return *_pe;
}
E * _pe;
};
// To test
struct sub_ee_ref : ee_ref<char>{};
using namespace std;
#include <iostream>
#include <memory>
#include <vector>
int main(void)
{
cout << "Expect Yes" << endl;
cout << has_const_reference_op<auto_ptr<int>,int &>::value;
cout << has_const_reference_op<unique_ptr<int>,int &>::value;
cout << has_const_reference_op<shared_ptr<int>,int &>::value;
cout << has_const_reference_op<std::vector<int>::iterator,int &>::value;
cout << has_const_reference_op<std::vector<int>::const_iterator,
int const &>::value;
cout << has_const_reference_op<int_ref,int &>::value;
cout << has_const_reference_op<sub_int_ref,int &>::value << endl;
cout << "Expect No" << endl;
cout << has_const_reference_op<int *,int &>::value;
cout << has_const_reference_op<unique_ptr<int>,char &>::value;
cout << has_const_reference_op<unique_ptr<int>,int const &>::value;
cout << has_const_reference_op<unique_ptr<int>,int>::value;
cout << has_const_reference_op<unique_ptr<long>,int &>::value;
cout << has_const_reference_op<int,int>::value;
cout << has_const_reference_op<std::vector<int>,int &>::value;
cout << has_const_reference_op<ee_ref<int>,int &>::value;
cout << has_const_reference_op<sub_ee_ref,int &>::value;
cout << has_const_reference_op<empty,int &>::value << endl;
return 0;
}
Are there new flaws in this idea? Can it be made more generic without once again
falling foul of the snag it avoids?