How to detect existence of a class using SFINAE?
Still did not find a satisfying answer in this post...
Mike Kinghan started the answer right and told a smart thing:
So the problem is to determine whether T is a defined class type.
But
sizeof(T) is no help here
is not correct...
Here is how you can do it with sizeof(T)
:
template <class T, class Enable = void>
struct is_defined
{
static constexpr bool value = false;
};
template <class T>
struct is_defined<T, std::enable_if_t<(sizeof(T) > 0)>>
{
static constexpr bool value = true;
};
If we ask the compiler to tell us anything about a class type T
that has not
even been declared we are bound to get a compilation error. There is no way
around that. Therefore if we want to know whether class T
"exists", where T
might not even have been declared yet, we must declare T
first.
But that is OK, because merely declaring T
will not make it "exist", since
what we must mean by T
exists is T
is defined. And if, having declared T
,
you can then determine whether it is already defined, you need not be in
any confusion.
So the problem is to determine whether T
is a defined class type.
sizeof(T)
is no help here. If T
is undefined then it will give an
incomplete type T
error. Likewise typeid(T)
. Nor is it any good
crafting an SFINAE probe on the type T *
, because T *
is a defined type
as long as T
has been declared, even if T
isn't. And since we are
obliged to have a declaration of class T
, std::is_class<T>
is not the
answer either, because that declaration will suffice for it to say "Yes".
C++11 provides std::is_constructible<T ...Args>
in <type_traits>
. Can
this offer an off-the-peg solution? - given that if T
is defined, then it must have at least one constructor.
I'm afraid not. If you know the signature of at least one public
constructor of T
then GCC's <type_traits>
(as of 4.6.3) will indeed do
the business. Say that one known public constructor is T::T(int)
. Then:
std::is_constructible<T,int>::value
will be true if T
is defined and false if T
is merely declared.
But this isn't portable. <type_traits>
in VC++ 2010 doesn't yet provide std::is_constructible
and even its std::has_trivial_constructor<T>
will barf if T
is not defined: most likely when std::is_constructible
does arrive it will follow suit. Furthermore, in the eventuality that only private constructors of T
exist for offering to std::is_constructible
then even GCC
will barf (which is eyebrow raising).
If T
is defined, it must have a destructor, and only one destructor. And that destructor is likelier to be public than any other possible member of T
. In that light, the simplest and strongest play we can make is to craft an SFINAE probe for the existence of T::~T
.
This SFINAE probe cannot be crafted in the routine way for determining
whether T
has an ordinary member function mf
- making the "Yes overload"
of the SFINAE probe function takes an argument that is defined in terms
of the type of &T::mf
. Because we're not allowed to take the address of a
destructor (or constructor).
Nevertheless, if T
is defined, then T::~T
has a type DT
- which must be
yielded by decltype(dt)
whenever dt
is an expression that evaluates to an
invocation of T::~T
; and therefore DT *
will be a type also, that can in
principle be given as the argument type of a function overload. Therefore we
can write the probe like this (GCC 4.6.3):
#ifndef HAS_DESTRUCTOR_H
#define HAS_DESTRUCTOR_H
#include <type_traits>
/*! The template `has_destructor<T>` exports a
boolean constant `value that is true iff `T` has
a public destructor.
N.B. A compile error will occur if T has non-public destructor.
*/
template< typename T>
struct has_destructor
{
/* Has destructor :) */
template <typename A>
static std::true_type test(decltype(std::declval<A>().~A()) *) {
return std::true_type();
}
/* Has no destructor :( */
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)) type;
static const bool value = type::value; /* Which is it? */
};
#endif // EOF
with only the restriction that T
must have a public destructor to be legally invoked in the argument expression of decltype(std::declval<A>().~A())
. (has_destructor<T>
is a simplified adaptation of the method-introspecting template I contributed here.)
The meaning of that argument expression std::declval<A>().~A()
may be obscure to some, specifically std::declval<A>()
. The function template std::declval<T>()
is defined in <type_traits>
and returns a T&&
(rvalue-reference to T
) - although it may only be invoked in unevaluated contexts, such as the argument of decltype
. So the meaning of std::declval<A>().~A()
is a call to ~A()
upon some given A
. std::declval<A>()
serves us well here by obviating the need for there to be any public constructor of T
, or for us to know about it.
Accordingly, the argument type of the SFINAE probe for the "Yes overload" is: pointer to the type of the destructor of A
, and test<T>(0)
will match that overload just in case there is such a type as destructor of A
, for A
= T
.
With has_destructor<T>
in hand - and its limitation to publicly destructible values of T
firmly in mind - you can test whether a class T
is defined at some point in your code by ensuring that you declare it before asking the question. Here is a test program.
#include "has_destructor.h"
#include <iostream>
class bar {}; // Defined
template<
class CharT,
class Traits
> class basic_iostream; //Defined
template<typename T>
struct vector; //Undefined
class foo; // Undefined
int main()
{
std::cout << has_destructor<bar>::value << std::endl;
std::cout << has_destructor<std::basic_iostream<char>>::value
<< std::endl;
std::cout << has_destructor<foo>::value << std::endl;
std::cout << has_destructor<vector<int>>::value << std::endl;
std::cout << has_destructor<int>::value << std::endl;
std::count << std::has_trivial_destructor<int>::value << std::endl;
return 0;
}
Built with GCC 4.6.3, this will tell you that the 2 // Defined
classes
have destructors and the 2 // Undefined
classes do not. The fifth
line of output will say that int
is destructible, and the final
line will show that std::has_trivial_destructor<int>
agrees. If we want
to narrow the field to class types, std::is_class<T>
can be applied after
we determine that T
is destructible.
Visual C++ 2010 does not provide std::declval()
. To support that compiler
you can add the following at the top of has_destructor.h
:
#ifdef _MSC_VER
namespace std {
template <typename T>
typename add_rvalue_reference<T>::type declval();
}
#endif