28

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.

Community
  • 1
  • 1
  • 1
    The `boost::array` class template is an aggregate too. So you can do `array a = { "foo", "bar" };` with it, for example. Also, my lazy-construct-array is an aggregate too: http://stackoverflow.com/questions/2662417/c-suppress-automatic-initialization-and-destruction/2662526#2662526 – Johannes Schaub - litb May 14 '10 at 23:04
  • 3
    Implicit conversions + aggregates... ಠ_ಠ – Steve Guidi May 14 '10 at 23:21
  • @litb when I first saw that feature of `boost::array` I had an enlightenment, AKA sexual-satisfaction-of-the-brain. Simple things that make so much sense tend to do that to me. – wilhelmtell May 14 '10 at 23:54

5 Answers5

29

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.

AnT stands with Russia
  • 312,472
  • 42
  • 525
  • 765
  • §12.6.1 is also relevant, as noted in § 8.5.1 13. – outis May 14 '10 at 23:10
  • @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 :) – AnT stands with Russia May 14 '10 at 23:13
  • 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 May 15 '10 at 00:12
  • it specifies when class members of an aggregate are copy-initialized, value-initialized or that the initializer is ill-formed. – outis May 16 '10 at 03:20
8

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.

Community
  • 1
  • 1
Michael Burr
  • 333,147
  • 50
  • 533
  • 760
3

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
  • 339,232
  • 124
  • 596
  • 636
2

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.

1

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
  • 57,473
  • 20
  • 96
  • 131
  • 1
    If I'm not mistaken, violating a precondition guarantees undefined behavior, not an exception. – James McNellis May 14 '10 at 23:50
  • 1
    @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 May 15 '10 at 00:18