28

I am using a map as an associative array of IDs -> value, where the value is a struct defining the object:

#include <map>

struct category {
        int id;
        std::string name;
};

std::map<int, category> categories;

int main() {
        categories[1] = {1, "First category"};
        categories[2] = {2, "Second category"};

}

The above code compiles with g++, but with the following warning:

warning: extended initializer lists only available with -std=c++0x or -std=gnu++0x

I have read various questions/answers here about struct initialization, but I'm still a bit confused. I have a series of related questions:

  1. I could add the compiler option -std=c++0x and be done with the warning, but still be none the wiser about the underlying problem. Wouldn't things break if I add a method to the category struct?

  2. What would the best way be to initialize this POD struct (category) in a more C++03 compliant way?

  3. Basically, I am not yet sure of the consequences of doing things one way rather than another way. This kind of associative array (where the key is the ID of an object) is easy with PHP, and I'm still learning about the proper way to do it in C++. Is there anything I should pay attention to in the context of the code above?

Edit
The following questions are related, but I didn't understand the answers when I first read them:
C++ initialize anonymous struct
c++ Initializing a struct with an array as a member
Initializing structs in C++

Community
  • 1
  • 1
augustin
  • 14,373
  • 13
  • 66
  • 79

5 Answers5

27

In C++ (ISO/IEC 14882:2003), a brace enclosed list of expressions can be used to initialize a variable of aggregate type in the declaration that defines it.

E.g.

struct S { int a; std::string b; };

S x = { 39, "Hello, World\n" };

An aggregate type 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. Note that a class aggregate doesn't have to be a POD-class and any array is an aggregate whether or not the type that it is an array of is an aggregate.

However, a brace-enclosed list of expressions is only valid as an initializer for an aggregate, it is not generally allowed in other contexts such as assignment or a class constructor's member initialization list.

In the current draft of the next version of C++ (C++0x), a brace enclosed list of expressions (brace-init-list) is allowed in more contexts and when an object is initialized from such an initializer list it is called list-initialization.

New contexts where such a list is allowed include arguments in a function call, function returns, arguments to constructors, member and base initializers and on the right hand side of an assignment.

This means that this is not valid in C++03.

int main() {
        categories[1] = {1, "First category"};
        categories[2] = {2, "Second category"};
}

Instead you could do something like this.

int main() {
        category tmp1 = { 1, "First category" };
        category tmp2 = { 2, "Second category" };

        categories[1] = tmp1;
        categories[2] = tmp2;
}

Alternatively.

int main() {
        category tmpinit[] = { { 1, "First category" },
                               { 2, "Second category" } };
        categories[1] = tmpinit[0];
        categories[2] = tmpinit[1];
}

Or, you could consider making a factory function for your type. (You could add a constructor for your type but this would make your class a non-aggregate and would prevent you from using aggregate initialization in other places.)

category MakeCategory( int n, const char* s )
{
    category c = { n, s };
    return c;
}

int main()
{
    categories[1] = MakeCategory( 1, "First category" );
    categories[2] = MakeCategory( 2, "Second category" );
}
CB Bailey
  • 755,051
  • 104
  • 632
  • 656
  • Thank you @Charles Bailey for taking the time to summarize the whole discussion. Thank you @sbi for pushing this forward. I appreciate it all very much. :) – augustin Dec 11 '10 at 01:25
  • BTW, the title of the question finally does not reflect the content of the discussion. Feel free to edit the title of the question to something that would be more appropriate. – augustin Dec 11 '10 at 01:27
  • Holy crap, I never knew you didn't need a constructor to make something initializable in that way... Why did nobody ever tell me this!? – Willy Goat Mar 11 '17 at 21:31
13

In the current C++ standard, you can use initializer lists to initialize arrays and structs containing POD values only. The next standard (aka C++0x or C++1x) will allow to do the same on structs containing non-POD types, e.g. std::string. That's what the warning is about.

I'd suggest you add a simple constructor to category that takes the id and name and simply call that constructor instead:

#include <map>
#include <string>

struct category {
        category() : id(0), name() {}
        category(int newId, std::string newName)
         : id(newId), name(newName) {}

        int id;
        std::string name;
};

std::map<int, category> categories;

int main() {
        categories[1] = category(1, "First category");
        categories[2] = category(2, "Second category");

}
Mephane
  • 1,984
  • 11
  • 18
  • +1, thanks. It works this way. Strangely, the code does not compile if I omit the line with the empty contructor category::category(). Apparently map needs it for comparison purposes: /usr/include/c++/4.4/bits/stl_map.h:450: error: no matching function for call to ‘category::category()’ – augustin Dec 09 '10 at 09:55
  • 3
    That is not strange, but standard behaviour. Any type you want to store inside an STL container needs a default constructor. And classes and structs without any constructor get one automatically, but as soon as you define your own one, you'll also have to define a default one, too. – Mephane Dec 09 '10 at 10:24
  • 2
    @Mephane: I'd be careful with overloading the semantics of comparison operators, especially the equality. A user of your code may notice that you defined them, and not read the implementation (or specs). Also, for for finding an id you'll need to use a dummy category object. If consistency is your concern, I'd prefer `category tmpcat(id, name); categories[tmpcat.id] = tmpcat;` – davka Dec 09 '10 at 14:25
  • 1
    While the solution is right, the initial statement is wrong. C++ inherited aggregate initialization from C and that works even in C++03. This `struct X { int i, j; } = {42, 24};` should work in C++03 as well. The problem is that `std::string` isn't a POD, and thus `category` isn't either, which is why it can't be initialized in C++03 using this syntax. It is indeed true that C++1x will fix this. – sbi Dec 09 '10 at 15:42
  • @sbi: Ok, my bad; I was under the impression this construct isn't possible at all in the current standard, I will include this in the answers. – Mephane Dec 10 '10 at 07:22
  • 1
    It's not true that any type that you want to store inside a standard container needs a default constructor. A default constructor is required for some common operations in some containers (`op[]` in `std::map` is one example) but the majority of operations don't require the contained class type to be default constructible. – CB Bailey Dec 10 '10 at 07:40
  • 1
    @sbi: Whether you can initialize a type with an initializer list in C++03 depends only on whether it is an _aggregate_ and not on a type's PODness. It is perfectly OK in C++03 to do: `struct S { int a; std::string b; } x = { 39, "Hello, World\n" };` . What you can't do in C++03 is any form of _list-initialization_ where you use a brace enclosed list of expressions anywhere other than in a declaration of a object of aggregate type. – CB Bailey Dec 10 '10 at 07:48
  • @Charles: I suspected I wouldn't have this right the moment I types "aggregate-initialization" and "POD" in the same sentence, but didn't remember the details. Why don't you go and add an answer explaining this exactly? – sbi Dec 10 '10 at 08:54
  • @Mephane, could you complete your answer to take into account the latest discussion? – augustin Dec 10 '10 at 11:46
  • 1
    @sbi: OK, I've added an answer that (I hope) shows some simple C++03 ways to address the problem. Personally I don't favour adding a constructor as the type being a simple aggregate class may be useful in other places. A simple factory function is easy to add and is as almost certainly as good as a constructor as NRVO is almost a given with any decent compiler. – CB Bailey Dec 10 '10 at 16:18
  • @Charles, @Mephane, @augustin: I retract most of what I said. Charles' answer sums it up very nicely and without any of my errors. – sbi Dec 10 '10 at 19:30
  • @Charles Bailey, @Mephane, @sbi, @davka et all: thanks all. I've accordingly accepted Charles' answer. Thank you all for your contributions in this discussion. – augustin Dec 11 '10 at 01:23
6

I know this is old, but one can also use

std::map<int, std::pair<std::string, int>> categories

or:

std::map<int, std::tuple<std::string, int, double>> categories

if one needs more.

AKJ
  • 950
  • 2
  • 13
  • 18
4

the kind of initialization we are using is introduced only in the emerging C++ standard called C++0x, hence the warning and the compiler option. Some compilers, as g++, already support some of the new features, but the standard itself is not yet accepted. It adds many new features to C++ as we know it. You can read more on Stroustrup's site.

to initialize the structure you can add a ctor (naturally), e.g.

struct category {
        category(int i, const std::string& n): id(i), name(n) {}
        int id;
        std::string name;
};

and then to initialize the map as follows:

categories[1] = category(1, "First category");

note that an implicit conversion from const char* to string will work here, or else you can define a ctor with const char* also.

davka
  • 13,974
  • 11
  • 61
  • 86
  • and Mephane cross-posted about the same answer 1 minute apart. The answer is the same, but for the empty constructor, and the code won't compile without it. See my comment in Mephane's answer. In any case, it confirms that the use of a constructor is the way to go. Thanks. (and +1) – augustin Dec 09 '10 at 09:57
  • @augustin: correct re empty ctor. One note, I'd prefer the const ref parameter passing `const std::string& n` over `std::string n`, to avoid temp memory allocation and copying. And thanks for acknowledgment :) – davka Dec 09 '10 at 13:35
  • Good point about const std::string& n. And thanks for your help. – augustin Dec 09 '10 at 14:07
2

the feature you need is termed aggregate in C/C++. By searching "aggregate c++",you'll find a lot of information detailing the whys and hows.

1- Wouldn't things break if I add a method to the category struct?

Not necessary unless the method influences the underlying C++ memory layout. For example, a plain function does not matter, but a virtual function will because it's likely laid out before the class members.

2- What would the best way be to initialize this POD struct (category) in a more c99 compliant way?

using constructors as the other responders suggest.

3- Is there anything I should pay attention to in the context of the code above?

It may involve redundant copies depending how you design you constructor. but it only matters if you often you need the initialization and you really care about the performance.

t.g.
  • 1,719
  • 2
  • 14
  • 25
  • +1, thanks. That's something new to me, and I'm looking into it right now as you suggest. – augustin Dec 09 '10 at 10:00
  • Thanks to your suggestion, I found this useful page re. Aggregate initialization: http://www.codeguru.com/cpp/tic/tic0077.shtml – augustin Dec 10 '10 at 05:34