tags:

views:

570

answers:

5

As an addendum to this question, what is going on here:

#include <string>
using namespace std;

struct A {
    string s;
};

int main() {
    A a = {0};
}

Obviously, you can't set a std::string to zero. Can someone provide an explanation (backed with references to the C++ Standard, please) about what is actually supposed to happen here? And then explain for example):

int main() {
    A a = {42};
}

Are either of these well-defined?

Once again an embarrassing question for me - I always give my structs constructors, so the issue has never arisen before.

+25  A: 

Your struct is an aggregate, so the ordinary rules for aggregate initialization work for it. The process is described in 8.5.1. Basically the whole 8.5.1 is dedicated to it, so I don't see the reason to copy the whole thing here. The general idea is virtually the same it was in C, just adapted to C++: you take an initializer from the right, you take a member from the left and you initialize the member with that initializer. According to 8.5/12, this shall be a copy-initialization.

When you do

A a = { 0 };

you are basically copy-initializing a.s with 0, i.e. for a.s it is semantically equivalent to

string s = 0;

The above compiles because std::string is convertible from a const char * pointer. (And it is undefined behavior, since null pointer is not a valid argument in this case.)

Your 42 version will not compile for the very same reason the

string s = 42;

will not compile. 42 is not a null pointer constant, and std::string has no means for conversion from int type.

P.S. Just in case: note that the definition of aggregate in C++ is not recursive (as opposed to the definition of POD, for example). std::string is not an aggregate, but it doesn't change anything for your A. A is still an aggregate.

AndreyT
§12.6.1 is also relevant, as noted in § 8.5.1 13.
outis
@outis: I looked through 12.6.1 and I couldn't immediately see what it added to what was already in 8.5. Every time 12.6.1 deals with aggregate initialization, it seems to refer back to 8.5 :)
AndreyT
It's interesting to see that in `basic_string(size_type n, charT c, const Allocator a=Allocator())` there's a reason why `size_type n` doesn't have a default value. The reason is that it's a bad idea to overload on a pointer and an integer. The value 0 (zero) is strictly an integer, not a pointer, and so you wouldn't be able to construct through the pointer overload with a null pointer, unless of course you'd cast explicitly. The standard avoids this confusion by requiring a character type if you specify a string length at string construction.
wilhelmtell
@AndreyT: it specifies when class members of an aggregate are copy-initialized, value-initialized or that the initializer is ill-formed.
outis
+7  A: 

8.5.1/12 "Aggregates" says:

All implicit type conversions (clause 4) are considered when initializing the aggregate member with an initializer from an initializer-list.

So

A a = {0};

will get initialized with a NULL char* (as AndreyT and Johannes indicated), and

A a = {42};

will fail at compile time since there's no implicit conversion that'll match up with a std::string constructor.

Michael Burr
+2  A: 

As people have pointed out, this "works" because string has a constructor that can take 0 as a parameter. If we say:

#include <map>
using namespace std;

struct A {
    map <int,int> m;
};

int main() {
    A a = {0};
}

then we get a compilation error, as the map class does not have such a constructor.

anon
Why is your pic avatar not shown in this answer? Is it hidden on community answers?
Johannes Schaub - litb
@Johannes A mystery! Do you want to report it as a bug on meta or shall I?
anon
@Neil go for it :)
Johannes Schaub - litb
@Johannes Done.
anon
Your identity doesn't belong to you, it belongs to the Collective.
wilhelmtell
+1  A: 

In 21.3.1/9 the standard forbids the char* argument of the relevant constructor of a std::basic_string from being a null pointer. This should throw a std::logic_error, but I have yet to see where in the standard is the guarantee that violating a precondition throws a std::logic_error.

wilhelmtell
If I'm not mistaken, violating a precondition guarantees undefined behavior, not an exception.
James McNellis
@James g++ 4.0.1 on OS X 10.5.8 throws a `std::logic_error` at construction time. 19.1.1 says this is what `logic_error` is for, but I can't find a guarantee this is what happens when there's a violation of an invariant or precondition.
wilhelmtell
+3  A: 

0 is a null pointer constant

S.4.9:

A null pointer constant is an integral constant expression (5.19) rvalue of integer type that evaluates to zero.

A null pointer constant can be converted to any other pointer type:

S.4.9:

A null pointer constant can be converted to a pointer type; the result is the null pointer value of that type

What you gave for the definition of A is considered an aggregate:

S.8.5.1:

An aggregate is an array or a class with no user-declared constructors, no private or protected non-static data members, no base classes, and no virtual functions.

You are specifying an initializer clause:

S.8.5.1:

When an aggregate is initialized the initializer can contain an initializer-clause consisting of a brace enclosed, comma-separated list of initializer-clauses for the members of the aggregate

A contains a member of the aggregate of type std::string, and the initializer clause applies to it.

Your aggregate is copy-initialized

When an aggregate (whether class or array) contains members of class type and is initialized by a brace enclosed initializer-list, each such member is copy-initialized.

Copy initializing means that you have the equivalent to std::string s = 0 or std::string s = 42;

S.8.5-12

The initialization that occurs in argument passing, function return, throwing an exception (15.1), handling an exception (15.3), and brace-enclosed initializer lists (8.5.1) is called copy-initialization and is equivalent to the form T x = a;

std::string s = 42 will not compile because there is no implicit conversion, std::string s = 0 will compile (because an implicit conversion exists) but results in undefined behavior.

std::string's constructor for const char* is not defined as explicit which means you can do this: std::string s = 0

Just to show that things are actually being copy-initialized, you could do this simple test:

class mystring
{
public:

  explicit mystring(const char* p){}
};

struct A {
  mystring s;
};


int main()
{
    //Won't compile because no implicit conversion exists from const char*
    //But simply take off explicit above and everything compiles fine.
    A a = {0};
    return 0;
}
Brian R. Bondy