19

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.
Community
  • 1
  • 1
BlackHC
  • 592
  • 5
  • 10
  • 3
    Why not check if you already have it in the standard library as [`std::is_default_constructible`](http://en.cppreference.com/w/cpp/types/is_default_constructible)? – Some programmer dude Sep 11 '12 at 06:46
  • 2
    What doesn't work? It works fine on g++, except for `BOOST_STATIC_ASSERT( !is_default_constructible::value );` – BЈовић Sep 11 '12 at 06:48
  • @BЈовић If an assert is failing, and that assert is correct, it doesn't work fine, surely? –  Sep 11 '12 at 06:55
  • @hvd From the question, it is not clear what fails. Apparently, that assert doesn't fail for the OP (at least for the 1st version of the code). – BЈовић Sep 11 '12 at 06:56
  • @BЈовић Indeed, that's how I read the question too. Then on your system, the first version (and perhaps the second too) fails, while on the OP's system with Microsoft's compiler, only the second fails. –  Sep 11 '12 at 07:05
  • Exactly, the first version runs fine, ie passes all tests, while the second ones fails the int[100] and the NotDefaultConstructible tests. – BlackHC Sep 11 '12 at 07:11
  • 2
    g++-4.7.1 compiles both variants correctly. – ecatmur Sep 11 '12 at 10:20
  • On http://www.comeaucomputing.com/tryitout/ your sfinae does seemingly never work (nothing is default constructible for it). So it feels that some hack how compilers evaluate that `sizeof U()` is involved here. – Öö Tiib Sep 15 '12 at 17:35
  • 2
    You should use `typedef char yes[1]; typedef char no[2];` to guarantee they are different sizes (`int` and `char` could, in theory, be the same size); plus I find it easier to read. – GManNickG Sep 27 '12 at 20:45

2 Answers2

3

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.

DS.
  • 22,632
  • 6
  • 47
  • 54
3

(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 );
Quuxplusone
  • 23,928
  • 8
  • 94
  • 159
  • You've explained this very nicely. I've targetted it to fail `int[100]`, because I wrote a template function with one optional parameter `T defaultValue = T()`, which did not work for `T=int[100]` in VS2010. I didn't know about default- vs value-initialization until now. – BlackHC Sep 28 '12 at 09:15