11

The base components of my hobby library has to work with C++98 and C++11 compilers. To learn and to enjoy myself I created the C++98 implementations of several type support functionality (like enable_if, conditional, is_same, is_integral etc. ...) in order to use them when there is no C++11 support.

However while I was implementing is_constructible I got stuck. Is there any kind of template magic (some kind of SFINAE) with which I can implement it without C++11 support (declval)?

Of course there is no variadic template support in C++03, so I will specialise the implementation till some depth. The main question is if there is a technique that can decide whether T is constructible from the given types or not.

iammilind
  • 68,093
  • 33
  • 169
  • 336
Broothy
  • 659
  • 5
  • 20
  • 9
    Several compilers use a builtin `__is_constructible` helper, so it is probably very hard, at a minimum. – Bo Persson Nov 05 '16 at 16:03
  • Related: https://stackoverflow.com/questions/38181357/how-is-stdis-constructiblet-args-implemented – BartoszKP Nov 05 '16 at 18:19
  • Yes, the C++11 implementation is clear. I would like to eliminate the `decltype` and `std::declval` dependencies from the referred implementation with some kind of magic or find a completely different technique. The only restriction is the C++98 standard. – Broothy Nov 05 '16 at 18:55

4 Answers4

10

It's possible:

#include <iostream>

template<typename T, T Val>
struct integral_constant {
    typedef integral_constant type;
    typedef T value_type;
    enum {
        value = Val
    };
};

typedef integral_constant<bool, true> true_type;
typedef integral_constant<bool, false> false_type;

template<typename T>
struct remove_ref {
    typedef T type;
};

template<typename T>
struct remove_ref<T&> {
    typedef T type;
};

// is_base_of from https://stackoverflow.com/questions/2910979/how-does-is-base-of-work
namespace aux {
    typedef char yes[1];
    typedef char no[2];

    template <typename B, typename D>
    struct Host
    {
        operator B*() const;
        operator D*();
    };
}
template <typename B, typename D>
struct is_base_of
{
  template <typename T> 
  static aux::yes& check(D*, T);
  static aux::no& check(B*, int);

  static const bool value = sizeof(check(aux::Host<B,D>(), int())) == sizeof(aux::yes);
};

template<typename T>
struct remove_cv {
    typedef T type;
};
template<typename T>
struct remove_cv<const T> {
    typedef T type;
};
template<typename T>
struct remove_cv<volatile T> {
    typedef T type;
};
template<typename T>
struct remove_cv<const volatile T> {
    typedef T type;
};

template<typename T>
struct is_void : integral_constant<bool, false> {};
template<>
struct is_void<void> : integral_constant<bool, true> {};

template<class T>
struct type_identity {
    // Used to work around Visual C++ 2008's spurious error: "a function-style conversion to a built-in type can only take one argument"
    typedef T type;
};

template <bool, typename T, typename>
struct conditional {
    typedef T type;
};
template <typename T, typename U>
struct conditional<false, T, U> {
    typedef U type;
};


namespace aux {

template<typename T, typename U>
struct is_more_const : integral_constant<bool, false> {};

template<typename T, typename U>
struct is_more_const<const T, U> : integral_constant<bool, true> {};

template<typename T, typename U>
struct is_more_const<const T, const U> : integral_constant<bool, false> {};

template<typename T, typename U>
struct is_more_volatile : integral_constant<bool, false> {};

template<typename T, typename U>
struct is_more_volatile<volatile T, U> : integral_constant<bool, true> {};

template<typename T, typename U>
struct is_more_volatile<volatile T, volatile U> : integral_constant<bool, false> {};

template<typename T, typename U>
struct is_more_cv : integral_constant<bool, is_more_const<T,U>::value && is_more_volatile<T,U>::value> {};


    template<typename T>
    struct is_default_constructible {
        template<typename U>
        static yes& test(int(*)[sizeof(new U)]);
        template<typename U>
        static no& test(...);
        enum {
            value = sizeof(test<T>(0)) == sizeof(yes)
        };
    };    

    template<typename T, typename Arg>
    struct is_constructible_1 {
        template<typename U, typename Arg_>
        static yes& test(int(*)[sizeof(typename type_identity<U>::type(static_cast<Arg_>(*((typename remove_ref<Arg_>::type*)0))))]);
        template<typename U, typename Arg_>
        static no& test(...);
        enum {
            value = sizeof(test<T, Arg>(0)) == sizeof(yes)
        };
    };   

    // Base pointer construct from Derived Pointer
    template<typename T, typename U>
    struct is_constructible_1<T*, U*>
        : conditional<
            is_void<typename remove_cv<T>::type>::value,
            integral_constant<bool, true>,
            typename conditional<
                is_void<typename remove_cv<U>::type>::value,
                integral_constant<bool, false>,
                typename conditional<
                    is_more_cv<T, U>::value,
                    integral_constant<bool, false>,
                    is_base_of<T,U>
                >::type
            >::type
        >::type
    {};

    // Base pointer construct from Derived Pointer
    template<typename T, typename U>
    struct is_constructible_1<T&, U&>
        : conditional<
            is_more_cv<T, U>::value,
            integral_constant<bool, false>,
            is_base_of<T,U>
        >::type
    {};


    template<typename T, typename Arg1, typename Arg2>
    struct is_constructible_2 {
        template<typename U, typename Arg1_, typename Arg2_>
        static yes& test(int(*)[
            sizeof(typename type_identity<U>::type(
                static_cast<Arg1_>(*((typename remove_ref<Arg1_>::type*)0)),
                static_cast<Arg2_>(*((typename remove_ref<Arg2_>::type*)0))
                ))
            ]);
        template<typename U, typename Arg1_, typename Arg2_>
        static no& test(...);
        enum {
            value = sizeof(test<T, Arg1, Arg2>(0)) == sizeof(yes)
        };
    };
}

template<typename T, typename Arg1 = void, typename Arg2 = void>
struct is_constructible : integral_constant<bool, aux::is_constructible_2<T, Arg1, Arg2>::value> {

};

template<typename T, typename Arg>
struct is_constructible<T, Arg> : integral_constant<bool, aux::is_constructible_1<T, Arg>::value> {

};
template<typename T>
struct is_constructible<T> : integral_constant<bool, aux::is_default_constructible<T>::value> {

};

struct Foo {};
struct fuzz_explicit {};
struct fuzz_implicit {};
struct Fuzz {
    explicit Fuzz(fuzz_explicit);
    Fuzz(fuzz_implicit);
};
struct buzz_explicit {};
struct buzz_implicit {};
struct Buzz {
    explicit Buzz(buzz_explicit);
    Buzz(buzz_implicit);
};
struct Bar {
    Bar(int);
    Bar(int, double&);
    Bar(Fuzz);
    explicit Bar(Buzz);
};

struct Base {};
struct Derived : Base {};

#define TEST(X) std::cout << #X << X << '\n'

int main() {
    TEST((is_constructible<Foo>::value));
    TEST((is_constructible<Bar>::value));
    TEST((is_constructible<Foo, int>::value));
    TEST((is_constructible<Bar, int>::value));
    TEST((is_constructible<Foo, const Foo&>::value));
    TEST((is_constructible<Bar, Bar>::value));
    TEST((is_constructible<Bar, int, double>::value));
    TEST((is_constructible<Bar, int, double&>::value));
    TEST((is_constructible<Bar, int, const double&>::value));
    TEST((is_constructible<int*, void*>::value));
    TEST((is_constructible<void*, int*>::value));
    TEST((is_constructible<Base&, Derived&>::value));
    TEST((is_constructible<Derived*, Base*>::value));
    // via Fuzz
    TEST((is_constructible<Bar, fuzz_explicit>::value));
    TEST((is_constructible<Bar, fuzz_implicit>::value));
    // via Buzz
    TEST((is_constructible<Bar, buzz_explicit>::value));
    TEST((is_constructible<Bar, buzz_implicit>::value));
    // integer promotion
    TEST((is_constructible<Bar, char>::value));
    // integer conversion
    TEST((is_constructible<Bar, unsigned long>::value));
}

You can expand the 2 parameters version for 3, 4, 5, ... parameters further more.

Live Demo


This works with g++ 4.4.7

It doesn't work with g++ 4.3.6

user541686
  • 205,094
  • 128
  • 528
  • 886
Danh
  • 5,916
  • 7
  • 30
  • 45
  • 2
    Your version employs expression SFINAE implicitly. While C++03 does not seem to (explicitly?) disallow it, many implementations did not support it back in the days. So not really a satisfying solution. – Columbo Nov 05 '16 at 19:52
  • @Columbo It works with g++ 4.4.7, but yes, it doesn't work with g++ 4.3.6 – Danh Nov 05 '16 at 19:58
  • what if the constructor is made private? – W.F. Nov 05 '16 at 20:06
  • 1
    What if the class deletes its allocation function? What if I ask `is_constructible`? – T.C. Nov 05 '16 at 20:13
  • @W.F. If the constructor is private the yes test function becomes ill-formed and the compiler ignores it. It is the proper behavior. – Broothy Nov 05 '16 at 22:11
  • @T.C. I updated new version, it fixed all your concerns. EDIT: I still don't know how to deal without `new T` for default constructible – Danh Nov 06 '16 at 05:58
  • @Broothy it's better now – Danh Nov 06 '16 at 05:58
  • @Danh I think the pointer-check part is a bit overengineered. I have updated my version, you can check it. You can check the default constructor part too, there is no operator new requirement. – Broothy Nov 06 '16 at 09:43
  • @Broothy No, it's **not** overengineered, your version can't check for default constructible, and it give the false detection for construction from `Derived*` to `Base*`, see [this](http://melpon.org/wandbox/permlink/hrTNgb0LrXJyBPnY) – Danh Nov 06 '16 at 09:53
  • @W.F. I still not figure out how to deal with private – Danh Nov 06 '16 at 10:00
  • @Danh I'm not sure if it's even possible I think it's the best you can do, but maybe you find some hacky way... I cross my fingers! – W.F. Nov 06 '16 at 10:04
  • 1
    @W.F. like Columbo said, this employs expression SFINAE implicitly, C++03 doesn't explicit disallow it, but C++ 11 allows it, in C++11, this kind of check can check for private access – Danh Nov 06 '16 at 10:06
  • I think there is no exact technique to detect the deleted functions. The standard is ambiguous, so it is unfortunately compiler specific. [link](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0348r0.html) – Broothy Nov 07 '16 at 15:32
  • 2
    Still fails for `is_constructible` given `enum X { };`. Also for `is_constructible`, and for pointers to member. This is basically why `is_constructible` is implemented by an intrinsic. – T.C. Nov 07 '16 at 21:03
  • @T.C. I've tested my implementation and is_constructible::value gives the right answer (true), but you're right, this implementation fails. I've checked the pointer-to-member cases and pointer-to-member-function cases too and both implementation gives the right answers (g++ 4.4.7). Could you give us a test case? With the enum both of our implementations fails. You're right, it should be part of the language itself (like in C++11), however it is an experiment, I would like to create the least bad C++98 implementation. – Broothy Nov 08 '16 at 15:08
5

I think Danh's idea was great! With a minor modification we can eliminate the operator new. (I have a C++98 enable_if and remove_reference implementation). The mentioned int*, void* case works with this implementation too. No operator new required. Only the old g++ support remains...

/********** std::remove_cv replacement **********/
template< typename T >
struct remove_const
{
    typedef T type;
};

template< typename T >
struct remove_const< const T >
{
    typedef T type;
};


template< typename T >
struct remove_volatile
{
    typedef T type;
};

template< typename T >
struct remove_volatile< volatile T >
{
    typedef T type;
};


template< typename T >
struct remove_cv
{
    typedef typename remove_volatile< typename remove_const< T >::type >::type type;
};


/********** std::is_pointer replacement *********/
template< typename T >
struct is_pointer_helper
{
    static const bool value = false;
};

template< typename T >
struct is_pointer_helper< T* >
{
    static const bool value = true;
};

template< typename T >
struct is_pointer
{
    static const bool value = is_pointer_helper< typename remove_cv< T >::type >::value;
};


/********** std::enable_if replacement **********/
template< bool CONDITION, typename TYPE = void >
struct enable_if
{
};

template< typename TYPE >
struct enable_if< true, TYPE >
{
    typedef TYPE type;
};


/****** std::remove_reference replacement *******/
template< typename T >
struct remove_reference
{
    typedef T type;
};

template< typename T >
struct remove_reference< T& >
{
    typedef T type;
};


/******* std::is_constructible replacement ******/
template< typename T, typename AT_1 = void, typename AT_2 = void, typename AT_3 = void, typename AT_4 = void >
class is_constructible_impl
{
private:
    template< typename C_T, typename C_AT_1, typename C_AT_2, typename C_AT_3, typename C_AT_4 >
    static bool test(
        typename c_std::enable_if<
            sizeof( C_T ) ==
            sizeof( C_T(
                static_cast< C_AT_1 >( *static_cast< typename c_std::remove_reference< C_AT_1 >::type* >( NULL ) ),
                static_cast< C_AT_2 >( *static_cast< typename c_std::remove_reference< C_AT_2 >::type* >( NULL ) ),
                static_cast< C_AT_3 >( *static_cast< typename c_std::remove_reference< C_AT_3 >::type* >( NULL ) ),
                static_cast< C_AT_4 >( *static_cast< typename c_std::remove_reference< C_AT_4 >::type* >( NULL ) )
            ) )
        >::type*
    );

    template< typename, typename, typename, typename, typename >
    static int test( ... );

public:
    static const bool value = ( sizeof( test< T, AT_1, AT_2, AT_3, AT_4 >( NULL ) ) == sizeof( bool ) );
};

template< typename T, typename AT_1, typename AT_2, typename AT_3 >
class is_constructible_impl< T, AT_1, AT_2, AT_3, void >
{
private:
    template< typename C_T, typename C_AT_1, typename C_AT_2, typename C_AT_3 >
    static bool test(
        typename c_std::enable_if<
            sizeof( C_T ) ==
            sizeof( C_T(
                static_cast< C_AT_1 >( *static_cast< typename c_std::remove_reference< C_AT_1 >::type* >( NULL ) ),
                static_cast< C_AT_2 >( *static_cast< typename c_std::remove_reference< C_AT_2 >::type* >( NULL ) ),
                static_cast< C_AT_3 >( *static_cast< typename c_std::remove_reference< C_AT_3 >::type* >( NULL ) )
            ) )
        >::type*
    );

    template< typename, typename, typename, typename >
    static int test( ... );

public:
    static const bool value = ( sizeof( test< T, AT_1, AT_2, AT_3 >( NULL ) ) == sizeof( bool ) );
};

template< typename T, typename AT_1, typename AT_2 >
class is_constructible_impl< T, AT_1, AT_2, void, void >
{
private:

    template< typename C_T, typename C_AT_1, typename C_AT_2 >
    static bool test(
        typename c_std::enable_if<
            sizeof( C_T ) ==
            sizeof( C_T(
                static_cast< C_AT_1 >( *static_cast< typename c_std::remove_reference< C_AT_1 >::type* >( NULL ) ),
                static_cast< C_AT_2 >( *static_cast< typename c_std::remove_reference< C_AT_2 >::type* >( NULL ) )
            ) )
        >::type*
    );

    template< typename, typename, typename >
    static int test( ... );

public:
    static const bool value = ( sizeof( test< T, AT_1, AT_2 >( NULL ) ) == sizeof( bool ) );
};

template< typename T, typename AT_1 >
class is_constructible_impl< T, AT_1, void, void, void >
{
private:
    template< typename C_T, typename C_AT_1 >
    static bool test(
        typename c_std::enable_if<
            sizeof( C_T ) ==
            sizeof( C_T(
                static_cast< C_AT_1 >( *static_cast< typename c_std::remove_reference< C_AT_1 >::type* >( NULL ) )
            ) )
        >::type*
    );

    template< typename, typename >
    static int test( ... );

public:
    static const bool value = ( sizeof( test< T, AT_1 >( NULL ) ) == sizeof( bool ) );
};

template< typename T >
class is_constructible_impl< T, void, void, void, void >
{
private:
    template< typename C_T >
    static C_T testFun( C_T );

    template< typename C_T >
    static bool test( typename c_std::enable_if< sizeof( C_T ) == sizeof( testFun( C_T() ) ) >::type* );

    template< typename >
    static int test( ... );

public:
    static const bool value = ( sizeof( test< T >( NULL ) ) == sizeof( bool ) );
};

template< typename T, typename AT_1 = void, typename AT_2 = void, typename AT_3 = void, typename AT_4 = void >
class is_constructible_impl_ptr
{
public:
    static const bool value = false;
};

template< typename T, typename AT_1 >
class is_constructible_impl_ptr< T, AT_1, typename enable_if< is_pointer< typename remove_reference< T >::type >::value, void >::type, void, void >
{
private:
    template< typename C_T >
    static bool test( C_T );

    template< typename >
    static int test( ... );

public:
    static const bool value = ( sizeof( test< T >( static_cast< AT_1 >( NULL ) ) ) == sizeof( bool ) );
};

template< typename T >
class is_constructible_impl_ptr< T, void, void, void, void >
{
public:
    static const bool value = true;
};

template< typename T, typename AT_1 = void, typename AT_2 = void, typename AT_3 = void, typename AT_4 = void >
class is_constructible
{
public:
    static const bool value = (
        is_pointer< typename remove_reference< T >::type >::value ?
            is_constructible_impl_ptr< T, AT_1, AT_2, AT_3, AT_4 >::value :
            is_constructible_impl< T, AT_1, AT_2, AT_3, AT_4 >::value
    );
};
Broothy
  • 659
  • 5
  • 20
  • nice one, but I'm still getting hard error when constructor is private [example](https://godbolt.org/g/UpV2qb) – W.F. Nov 06 '16 at 09:38
  • Hmmm, it sounds interesting because I have checked this case too. What compiler do you use? – Broothy Nov 06 '16 at 09:47
  • @Broothy It give incorrect value for default constructible, see http://melpon.org/wandbox/permlink/hrTNgb0LrXJyBPnY – Danh Nov 06 '16 at 09:51
  • @Broothy the example uses gcc 4.4.7, but it also does not compile on gcc 4.5.2, 4.6.4, 4.7, gcc compiles it since 4.8. Also clang in each version fails... – W.F. Nov 06 '16 at 09:52
  • From my observe: `(is_constructible::value)1`, `(is_constructible::value)1`, `(is_constructible::value)0` – Danh Nov 06 '16 at 09:55
  • @Danh I've fixed it. I'm sorry, I was in a rush. I see your problem with the default construction without new. The pointer check remained compact and I hope it is proper now. – Broothy Nov 06 '16 at 10:37
  • @Broothy yes, without new, `T()` is intepreted as function – Danh Nov 06 '16 at 10:53
  • I think I have found the trick to avoid new. I have updated my solution. – Broothy Nov 06 '16 at 11:04
  • nice! works exactly as ```std::is_constructible<>``` with clang/gcc - except for with Visual Studio (tried only VS 2017) - there the following code gives false: ```struct A { A (int,int) {}; };``` ```is_constructible::value``` - any idea how to fix it? – onqtam Apr 06 '17 at 10:44
  • 1
    You're right, I didn't check my implementation with VS compilers. I will check and try to fix it. – Broothy Apr 07 '17 at 07:27
4

To implement a fully conforming is_constructible, compiler support is necessary. The issue is not variadic template simulation or select idiom(sizeof over decltype).

Actually even before gcc 8.x(4.x to 7.x), there is a bug on is_constructible<To, From>, because it is implemented purely by library code. The bug happens when To is a reference type(i.e. T& or T&&). The same applies to clang libc++'s library version __libcpp_is_constructible<To, From>, but clang has compiler support for __is_constructible() since the support of c++11 so that's never a real issue.

The non-conforming cases are, when constructing a reference, the pure library implementation used by both clang(libc++) and gcc(libstdc++) uses SFINAE to check if static_cast<To>(declval<From>()) is well-formed. But there are two scenarios where you must explicitly use cast instead of initialization syntax(i.e. T t(args...)):

  1. When cast from a reference of base class to a reference of derived class:
    static_cast<Derived&>(declval<Base&>()) is valid, but you must always explicitly use cast, i.e. Base& bref; Derived& dref = bref; doesn't work, you must use Derived& dref = static_cast<Derived&>(bref).
  2. When cast from a lvalue reference to a rvalue reference:
    static_cast<A&&>(declval<A&>()) is valid (your familiar std::move()), but you must always explicitly use cast, i.e. A& lref; A&& ref = lref; doesn't work, you must use A&& ref = static_cast<A&&>(lref);(i.e. A&& ref = std::move(lref);)

In order to address such false positives, in addition to SFINAE conversion check, extra checks already exist in libc++ and libstdc++ to ensure the cast conversion is neither of the scenarios above.

But this introduces a new problem: if there exists user-defined (explicit) conversion, __is_constructible() is valid. But when the conversion is also one of the scenarios above, false negative happens.

For example, the code below demonstrates the base to derived reference conversion scenario. convert Base& to D1& or D2& needs explicit cast, but, there is also an user-defined explicit conversion that converts Base(&) to D1&. So is_constructible<D1&, Base&>::value evaluates to true, whereas is_constructible<D2&, Base&>::value evaluates to false.

struct D1;
struct D2;
struct Base {
    explicit operator D1&();
};

struct D1 : Base {
    D1(const D1&) = delete;
};
struct D2 : Base {};

int BtoD1() { // should be true
    return std::is_constructible<D1&, Base&>::value;
}
int BtoD2() { // should be false
    return std::is_constructible<D2&, Base&>::value;
}

But the library implementation reported both as false. godbolt link try it at you own. You can switch between clang / gcc(<7) / gcc(>=8) to see how the results change.

YumeYao
  • 557
  • 4
  • 10
0

The answers above are fantastic. However, it may be hard for newbies to understand.

Here is a very very simple solution, though it sacrifices most portability.

#include <cctype>

template<typename T>
struct is_default_constructible {
    template<typename U>
        static int8_t test(int(*)[sizeof(new U)]);
    template<typename U>
        static int16_t test(...);
    enum {
        value = sizeof(test<T>(0)) == 1
    };
};

Here is a demo

class Test1 {
public:
    Test1() = delete;
};
class Test2 {
public:
    Test2();
};

int main() {
    std::cout << is_default_constructible<Test1>::value
        << is_default_constructible<Test2>::value;
}