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.
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...)
):
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)
.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.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With