How to detect existence of a class using SFINAE?
Asked Answered
B

4

36

Is it possible to detect if a class exists in C++ using SFINAE? If possible then how?

Suppose we have a class that is provided only by some versions of a library. I'd like to know if it is possible to use SFINAE to detect whether the class exists or not. The result of detection is arbitrary, say an enum constant which is 1 if it exists, 0 otherwise.

Bushweller answered 23/5, 2012 at 0:32 Comment(8)
You mean to detect if a class contains a particular type? Or if a type exists in a namespace scope? (I don't see how the latter would be useful)Aristocratic
The compiler will do that for you. Please explain what you want to do?Risa
To clarify - the class is just a hard-coded identifier, so it doesn't depend on template parameter substitution at all?Retroflex
@sbabbi: A class at namespace scope. It can be very useful as a replacement/addition to configure-like build step.Bushweller
@ndkrempel: Yes, plain class, not a template if I understand you correctly.Bushweller
@JesseGood: I expanded the question a bit. Hope it makes things clearer.Bushweller
Isn't that exactly what the versioning system is for? Are you trying to get rid of a bunch of #ifdefs?Antistrophe
@dirkgently: In an ideal world, yes, this could be handled by checking the version macros or something like that. Unfortunately library designers do not always provide this kind of information.Bushweller
N
41

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
Numbers answered 23/5, 2012 at 15:11 Comment(6)
That is indeed a nice analysis. I think it's effectively equivalent to the solution I proposed except it uses the destructor as a hook instead of requiring a special conversion constructor. There's a trade-off there - this solution will work transparently most of the time, but mine will work all of the time at the cost of invoking a macro in each class definition.Rosetta
Hi, excellent answer there, I'm trying to modify your code to work with class templates, so far I get it to compile by changing 'typename A/T' to 'template<unsigned char, class> class A/T' to match my template's arguments but the test silently fails even when there's a public destructor. Do you know why that might be please? I know I could instantiate the template to a class and use the original but I'm using this check in a macro just before specialising the template, so I can't instantiate it before my specialisation. Thank you!Linnet
Impossible to say without seeing your code. Adapting the solution to a class template works straightforwardly for me, so there's something wrong with your code. I suggest you ask a new question with a suitable title and say: "I am trying to unsuccessfully to adapt <this solution> to work for a class template. Here is my code..."Numbers
I'm trying to get this to work, but can't - the "true" overload never appears, i.e., is always eliminated by SFINAE. Here's what I figured out: The type inside the delctype(…), i.e., the return type of the destructor, is void &. And it's not possible to create a pointer to it. Here's what my GCC tells me: cannot declare pointer to 'using DestructorReturnType = void&' {aka 'void&'}.Pinstripe
After further investigation, this is a bug in GCC 9.1: gcc.gnu.org/bugzilla/show_bug.cgi?id=90598#c0 All other compilers should be fine.Pinstripe
@Lukas did you find a workaround for that issue?Witty
T
23

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;
};
Teleutospore answered 9/8, 2017 at 15:4 Comment(5)
This answer is simple to understand and works for me on both GCC 7.2.1 and VS 2017. Whereas the top rated answer gives me "incomplete type" errors on GCC. Kudos for this :)Ovoviviparous
From standart "The sizeof operator shall not be applied to an expression that has function or incomplete type, to an enumeration type whose underlying type is not fixed before all its enumerators have been declared, to the parenthesized name of such types, or to a glvalue that designates a bit-field."Fontanez
@IsaacPascual yeap, and that is exactly why it works: the second substitution fails, when the type is incomplete. Actually, this could be renamed to is_complete but class-wise it is just is_defined. BTW, after a year I've just realized, that sizeof(T) > 0 can be replaced with simply sizeof(T).Teleutospore
Just what I was looking for. Thanks! Might I suggest that they inherit from std::false_type and std::true_type, so that they automatically provide operator()?Cladding
I've tried your solution, but I'm still faced with a chicken and egg problem where I can't pass the name of the class to the metafunction without triggering compiler errors.Cladding
P
10

With SFINAE, no. I think name lookup tricks are the way to get this done. If you aren't afraid to inject a name into the library's namespace:

namespace lib {
#if DEFINE_A
class A;
#endif
}

namespace {
    struct local_tag;
    using A = local_tag;
}

namespace lib {
    template <typename T = void>
    A is_a_defined();
}

constexpr bool A_is_defined =
  !std::is_same<local_tag, decltype(lib::is_a_defined())>::value;

Demo.

If A is declared in the global namespace:

#if DEFINE_A
class A;
#endif

namespace {
    struct local_tag;
    using A = local_tag;
}

namespace foo {
    template <typename T = void>
    ::A is_a_defined();
}

constexpr bool A_is_defined =
  !std::is_same<local_tag, decltype(foo::is_a_defined())>::value;

Demo.

Plead answered 11/11, 2014 at 23:4 Comment(1)
This is exactly what I was trying to figure out on my own, but got stuck with introducing the tagged definitions at global namespace. The benefit of this solution over the others I see here is that this solution does not require an accurate forward-declaration of the given type. In my use-case I don't know if the name I am looking up is a using directive, a class, or a template declaration which then means that I can't forward declare it properly. However, this solution works for my niche case. 3 excellent answers, for slightly different problems but for me this is the solution.Heifetz
R
0

Alright, I think I found a way to do this, though there may be better ways. Suppose we have class A that is included in some instances of the library and not in others. The trick is to define a special private conversion constructor in A and then use SFINAE to detect the conversion constructor. When A is included, the detection succeeds; when it's not, detection fails.

Here's a concrete example. First the header for the detection template, class_defined.hpp:

struct class_defined_helper { };

template< typename T >
struct class_defined {

  typedef char yes;
  typedef long no;

  static yes test( T const & );
  static no  test( ... );

  enum { value = sizeof( test( class_defined_helper( )) == sizeof( yes ) };
};

#define CLASS_DEFINED_CHECK( type )     \
  type( class_defined_helper const & ); \
                                        \
  friend struct class_defined< type >;

Now a header that contains a class definition, blah.hpp:

#include "class_defined.hpp"

#ifdef INCLUDE_BLAH
class blah {
  CLASS_DEFINED_CHECK( blah );
};
#else
class blah;
#endif

Now the source file, main.cpp:

#include "blah.hpp"

int main( ) {
  std::cout << class_defined< blah >::value << std::endl;
}

Compiled with BLAH_INCLUDED defined this prints 1. Without BLAH_INCLUDED defined it prints 0. Unfortunately this still requires a forward declaration of the class to compile in both cases. I don't see a way to avoid that.

Rosetta answered 23/5, 2012 at 1:45 Comment(3)
This does not really detect whether the type is defined (or even declared), but rather whether a macro is defined. In this case, it might be simpler just to drop all the code and leave it as #if CLASS_DEFINED_MACRO and then the dependent code.Cilo
I don't follow. The point was that it's impossible to check whether a type is declared since in order to check that you would need to declare it. I think it's also impossible to determine if a type is defined unless you have a hook to look for in the type. The macro provides that hook in the form of a conversion constructor. My solution allows you to compile a library with with and without a class definition and check whether the class is defined using SFINAE. That's as close as I think we can get to what the OP wants. Many patterns require hooks in types. That's all the macro us here.Rosetta
Except it has nothing to do with SFINAE.Molini

© 2022 - 2024 — McMap. All rights reserved.