25

I recently stumbles across some problem with initializer lists. Consider a program that stores map-like data

struct MyMapLike {
  MyMapLike(std::map<std::string, int> data)
    :data(std::move(data))
  { }

private:
  std::map<std::string, int> data;
};

That looks straight forward. But when initializing it, it becomes ugly. I want to let it look like

MyMapLike maps = { { "One", 1 }, { "Two", 2 } };

But the compiler doesn't want to accept this, because the above means that it should look for a two-parameter constructor that can accept { "One", 1 } and { "Two", 2 } respectively. I need to add extra braces, to make it look like a single-parameter constructor accepting the { { ... }, { ... } }

MyMapLike maps = { { { "One", 1 }, { "Two", 2 } } };

I would not like to write it like that. Since I have a map-like class, and the initializer has the abstract value of a mapping-list, I would like to use the former version, and be independent of any such implementation details like level of nesting of constructors.

One work around is to declare an initializer-list constructor

struct MyMapLike {
  MyMapLike(std::initializer_list< 
    std::map<std::string, int>::value_type
    > vals)
    :data(vals.begin(), vals.end())
  { }

  MyMapLike(std::map<std::string, int> data)
    :data(std::move(data))
  { }

private:
  std::map<std::string, int> data;
};

Now I can use the former, because when I have an initializer-list constructor, the whole initializer list is treated as one element instead of being splitted into elements. But I think this separate need of the constructor is dead ugly.

I'm looking for guidance:

  • What do you think about the former and latter form of initialization? Does it make sense to be required to have extra braces in this case?
  • Do you consider the requirement for addition of an initializer list constructor in this case bad?

If you agree with me on that the former way of initialization is nicer, what solutions can you think of?

Johannes Schaub - litb
  • 496,577
  • 130
  • 894
  • 1,212
  • @MooingDuck, I think he's doing exactly that! See the `::value_type`. – Aaron McDaid Dec 21 '13 at 20:11
  • The "extra" outer `{}`'s are doing something different from the inner ones: they are part of the brace-initialization syntax, indicating that a constructor call is taking place, but are *not* part of the actual object being passed to the constructor. The inner braces, meanwhile, indicate the actual beginning of the initializer-list for the map. I think this makes perfect sense, and in fact Clang warns about certain (legal) elisions of the outermost set of braces, so I expect `{{ /* ... stuff ... */ }}` will become fairly standard over time among people who prefer brace-initialization syntax. – Kyle Strand Dec 15 '15 at 21:34

6 Answers6

10

Since I have a map-like class, and the initializer has the abstract value of a mapping-list, I would like to use the former version

And herin lies the problem: it's up to you to supply the constructors that allow your class to be treated like a map. You called your solution a work-around, but there's nothing to work around. :)

But I think this separate need of the constructor is dead ugly.

It is, but unfortunately since it's your class, you have to specify how the initializer lists work.

GManNickG
  • 494,350
  • 52
  • 494
  • 543
  • 3
    Sometimes I don't know it though. When I have a generic class that has a constructor taking `T`, I don't know how to write the initializer-list constructor for it :( – Johannes Schaub - litb Apr 21 '11 at 17:08
  • @JohannesSchaub-litb I'm not sure what you mean by a constructor taking `T`-- do you mean something like `template Foo::Foo(const T&)`? Or something like `template template Foo::Foo(T2&&)`? – Kyle Strand Dec 15 '15 at 21:37
6

What do you think about the former and latter form of initialization? Does it make sense to be required to have extra braces in this case?

I think so. I think it would permit too much ambiguity to allow a constructor be called not just when the syntax matches that constructor, but when the syntax matches some constructor of the single argument to the constructor, and so on recursively.

struct A { int i; };
struct B { B(A) {}  B(int) {} };
struct C { C(B) {} };

C c{1};

Do you consider the requirement for addition of an initializer list constructor in this case bad?

No. It lets you get the syntax you want but without creating the problems that arise if we make the compiler search harder for a constructor to use.

bames53
  • 86,085
  • 15
  • 179
  • 244
4

Wouldn't something like this give the desired effect (MyMapLike can be constructed in any way that std::map can, but does not implicitly convert to std::map)?

struct MyMapLike : private std::map<std::string, int>
{
    using map::map;
};

If it absolutely positively has to be a member, maybe use constructor perfect forwarding (I'm not sure about the exact syntax) along the lines of:

struct MyMapLike
{
    template<typename... Initializers>
    MyMapLike(Initializers... init, decltype(new std::map<std::string, int>(...init)) = 0)
      : data(...init)
    { }

private:
    std::map<std::string, int> data;
};
Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • Yes, but inheritance is not an option. For example, I consider having a `variant<>` that could contain both a `map` and an `int`. The code in my question is an example, it's not the ultimate problem I'm trying to solve. – Johannes Schaub - litb Apr 20 '11 at 21:16
  • Ok, how about a set of template parameter pack forwarding constructors, one for each member in your union, and using `enable_if` to make sure that the appropriate one (and only one) gets enabled? I don't know the exact syntax for that, but I'll try... – Ben Voigt Apr 20 '11 at 21:41
  • It'd be good to update this answer, now that the syntax is better known. – bames53 Apr 02 '12 at 19:54
  • @bames53: Feel free to do so. – Ben Voigt Apr 02 '12 at 20:13
  • Looks like perfect forwarding actually can't work here. I added an answer with the explanation. – bames53 Apr 02 '12 at 22:33
1

You could initialize as follows:

MyMapLike maps ( { { "One", 1 }, { "Two", 2 } } );

The outer ( ... ) are now clearly simply to surround the constructor args, and it's easier to see that the outermost { ... } defines the 'list'.

It's still a bit verbose, but it avoids the situation where { is being used to do two different things, which affects readability.

Aaron McDaid
  • 26,501
  • 9
  • 66
  • 88
0

I agree that it's ugly. I usually scrap initializer lists once I move beyond very simple cases as they're quite limited. For your case I'd go with boost::assign, which while not as terse gives better flexibility and control.

Ylisar
  • 4,293
  • 21
  • 27
0

What do you think about the former and latter form of initialization? Does it make sense to be required to have extra braces in this case?

I prefer the first form because it has a clean class interface. The constructor taking an initializer list pollutes the interface and offers little in return.

The extra braces are something we'll get used to. Just like we got used to C++'s other quirks.

StackedCrooked
  • 34,653
  • 44
  • 154
  • 278