Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does using two sizeofs work to check whether a class is default constructible, but one does not?

I used the code from "Is there a way to test whether a C++ class has a default constructor (other than compiler-provided type traits)?".

I modified it slightly to work with all my test cases:

template< class T >
class is_default_constructible {
    typedef int yes;
    typedef char no;


    // the second version does not work
#if 1
    template<int x, int y> class is_equal {};
    template<int x> class is_equal<x,x> { typedef void type; };

    template< class U >
    static yes sfinae( typename is_equal< sizeof U(), sizeof U() >::type * );
#else
    template<int x> class is_okay { typedef void type; };

    template< class U >
    static yes sfinae( typename is_okay< sizeof U() >::type * );
#endif

    template< class U >
    static no sfinae( ... );

public:
    enum { value = sizeof( sfinae<T>(0) ) == sizeof(yes) };
};

Why does it work correctly with the two template argument version but not with the normal one (set #if 0)? Is this a compiler bug? I'm using Visual Studio 2010.

I used the following tests:

BOOST_STATIC_ASSERT( is_default_constructible<int>::value );
BOOST_STATIC_ASSERT( is_default_constructible<bool>::value );
BOOST_STATIC_ASSERT( is_default_constructible<std::string>::value );
BOOST_STATIC_ASSERT( !is_default_constructible<int[100]>::value );

BOOST_STATIC_ASSERT( is_default_constructible<const std::string>::value );

struct NotDefaultConstructible {
    const int x;
    NotDefaultConstructible( int a ) : x(a) {}
};

BOOST_STATIC_ASSERT( !is_default_constructible<NotDefaultConstructible>::value );

struct DefaultConstructible {
    const int x;

    DefaultConstructible() : x(0) {}
};

BOOST_STATIC_ASSERT( is_default_constructible<DefaultConstructible>::value );

I'm really at a loss here:

  1. Two tests are failing with the other version: int[100] and NotDefaultConstructible. All the tests succeed with the two template argument version.
  2. Visual Studio 2010 does not support std::is_default_constructible. However, my question is about why there is any difference in the two implementations and why one works and the other does not.
like image 864
BlackHC Avatar asked Sep 11 '12 06:09

BlackHC


2 Answers

(My answer is greatly informed by DS's previous answer.)

First of all, notice that you have class is_okay { typedef void type; }, i.e., type is a private member of is_okay. This means that it's not actually visible outside the class and therefore

template< class U >
static yes sfinae( typename is_equal< sizeof U(), sizeof U() >::type * );

will never succeed. However, SFINAE didn't originally apply to this situation in C++98; it wasn't until the resolution to DR 1170 that "access checking [started being] done as part of the substitution process".[1]

(Amazingly, Paolo Carlini wrote that blog entry just 10 days ago, so your timing with this question is impeccable. In cases like this, according to Carlini, GCC prior to 4.8 didn't do access checking during SFINAE at all. So that explains why you didn't see GCC complaining about the private-ness of type. You'd have to be using a top-of-tree GCC from literally less than two weeks ago, in order to see the correct behavior.)

Clang (top-of-tree) follows the DR in -std=c++11 mode, but gives the expected error in its default C++03 mode (i.e. Clang doesn't follow the DR in C++03 mode). This is slightly odd but maybe they do it for backwards compatibility.

But anyway, you don't actually want type to be private in the first place. What you meant to write is struct is_equal and struct is_okay.

With this change, Clang passes all of your test cases. GCC 4.6.1 passes all of your test cases too, except for int[100]. GCC thinks that int[100] is okay, whereas you're asserting that it's not okay.

But another problem with your code is that it doesn't test what you think it's testing. The C++ standard, clause 8.5#10, says very clearly: [2]

An object whose initializer is an empty set of parentheses, i.e., (), shall be value-initialized.

So when you write sizeof U(), you're not testing if U can be default-initialized; you're testing if it can be value-initialized!

...Or are you? At least in some of my test cases, GCC's error messages indicated that U() was being interpreted as the name of a type — "function returning U" — and that was why int[100] behaved differently. I don't see how that behavior is valid, but I really don't understand the syntactic subtleties here.

If you really mean to test default initialization, you should use something like sizeof *new U everywhere you currently have sizeof U().

By the way, int[100] is default-initializable, period. The Standard is clear on what it means to default-initialize an array type.

Finally, I wonder if one cause of wacky behavior in your code is that you're trying to pass an unadorned 0 (which is of type int) to a function whose set of overloads includes one function taking void * and one taking .... I could totally understand if a compiler picked the wrong one in that case. You'd be better advised to try passing 0 to a function taking int.

Putting it all together, here's a version of your code that works perfectly for me (i.e., no assertion-failures) in both ToT Clang and GCC 4.6.1.

template< class T >
class is_default_constructible {
    typedef int yes;
    typedef char no;

    template<int x> struct is_okay { typedef int type; };

    template< class U >
    static yes sfinae( typename is_okay< sizeof (*new U) >::type );

    template< class U >
    static no sfinae( ... );

public:
    enum { value = sizeof( sfinae<T>(0) ) == sizeof(yes) };
};

#if __has_feature(cxx_static_assert)
#define BOOST_STATIC_ASSERT(x) static_assert(x, "or fail")
#else
#define dummy2(line) dummy ## line
#define dummy(line) dummy2(line)
#define BOOST_STATIC_ASSERT(x) int dummy(__COUNTER__)[(x) - 1]
#endif

#include <string>

BOOST_STATIC_ASSERT( !is_default_constructible<int()>::value );
BOOST_STATIC_ASSERT( is_default_constructible<bool>::value );
BOOST_STATIC_ASSERT( is_default_constructible<std::string>::value );
BOOST_STATIC_ASSERT( is_default_constructible<int[100]>::value );

BOOST_STATIC_ASSERT( is_default_constructible<const std::string>::value );

struct NotDefaultConstructible {
    const int x;
    NotDefaultConstructible( int a ) : x(a) {}
};

BOOST_STATIC_ASSERT( !is_default_constructible<NotDefaultConstructible>::value );

struct DefaultConstructible {
    const int x;

    DefaultConstructible() : x(0) {}
};

BOOST_STATIC_ASSERT( is_default_constructible<DefaultConstructible>::value );
like image 90
Quuxplusone Avatar answered Sep 27 '22 17:09

Quuxplusone


This seems almost certainly an artifact (bug) of the compiler, since g++ behaves (and fails) differently. I can only guess at why VS behaves differently, but one guess that seems reasonable is that this class:

template<int x> class is_okay { typedef void type; };

has the same definition regardless of the template parameter, so perhaps the compiler skips a step when analyzing static sfinae( typename is_okay< sizeof U() >::type * ); and considers it well-defined without looking closely at the parameter of is_okay. So then it thinks everything is default-constructible.

Why neither VS nor g++ is bothered by is_okay::type being private, I don't know. Seems like they both should be.

g++, on the other hand, treats both versions as equivalent. In both, however, it gives a different error for int[100]. That one is debatable as to whether it should be default-constructible. You seem to think it shouldn't be. g++47's std::is_default_constructible thinks it is! To get that behavior (which is likely more standard), you can replace T with typename boost::remove_all_extents<T>::type in the enum line.

like image 38
DS. Avatar answered Sep 27 '22 17:09

DS.