C++98/03 std::is_constructible implementation
Asked Answered
S

4

12

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.

Skintight answered 5/11, 2016 at 15:25 Comment(3)
Several compilers use a builtin __is_constructible helper, so it is probably very hard, at a minimum.Numeral
Related: #38181857Saltus
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.Skintight
W
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://mcmap.net/q/186601/-how-does-this-implementation-of-the-is_base_of-trait-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

Wini answered 5/11, 2016 at 19:33 Comment(15)
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.Refugee
@Refugee It works with g++ 4.4.7, but yes, it doesn't work with g++ 4.3.6Wini
what if the constructor is made private?Singletree
What if the class deletes its allocation function? What if I ask is_constructible<int*, void*>?Buttonhook
@Singletree If the constructor is private the yes test function becomes ill-formed and the compiler ignores it. It is the proper behavior.Skintight
@Buttonhook I updated new version, it fixed all your concerns. EDIT: I still don't know how to deal without new T for default constructibleWini
@Skintight it's better nowWini
@Wini 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.Skintight
@Skintight 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 thisWini
@Singletree I still not figure out how to deal with privateWini
@Wini 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!Singletree
@Singletree 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 accessWini
I think there is no exact technique to detect the deleted functions. The standard is ambiguous, so it is unfortunately compiler specific. linkSkintight
Still fails for is_constructible<X, int> given enum X { };. Also for is_constructible<const Bar&, Fuzz&>, and for pointers to member. This is basically why is_constructible is implemented by an intrinsic.Buttonhook
@Buttonhook I've tested my implementation and is_constructible<const Bar&, Fuzz&>::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.Skintight
S
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
    );
};
Skintight answered 5/11, 2016 at 22:6 Comment(10)
nice one, but I'm still getting hard error when constructor is private exampleSingletree
Hmmm, it sounds interesting because I have checked this case too. What compiler do you use?Skintight
@Skintight It give incorrect value for default constructible, see melpon.org/wandbox/permlink/hrTNgb0LrXJyBPnYWini
@Skintight 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...Singletree
From my observe: (is_constructible<int*, void*>::value)1, (is_constructible<Derived*, Base*>::value)1, (is_constructible<Bar, int, double&>::value)0Wini
@Wini 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.Skintight
@Skintight yes, without new, T() is intepreted as functionWini
I think I have found the trick to avoid new. I have updated my solution.Skintight
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<A,int,int>::value - any idea how to fix it?Noh
You're right, I didn't check my implementation with VS compilers. I will check and try to fix it.Skintight
S
5

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.

Spectatress answered 17/1, 2021 at 13:22 Comment(0)
M
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;
}
Margarettamargarette answered 15/4, 2021 at 13:26 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.