14

Since C++11, the Standard Library containers and std::string have constructors taking an initializer-list. This constructor takes precedence over other constructors (even, as pointed out by @JohannesSchaub-litb in the comments, even ignoring other "best match" criteria). This leads to a few well-known pitfalls when converting all parenthesized () forms of constructors to their braced versions {}

#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>
#include <string>

void print(std::vector<int> const& v)
{
    std::copy(begin(v), end(v), std::ostream_iterator<int>(std::cout, ","));
    std::cout << "\n";
}

void print(std::string const& s)
{
    std::cout << s << "\n";
}

int main()
{
    // well-known 
    print(std::vector<int>{ 11, 22 });  // 11, 22, not 11 copies of 22
    print(std::vector<int>{ 11 });      // 11,     not 11 copies of 0

    // more surprising
    print(std::string{ 65, 'C' });      // AC,     not 65 copies of 'C'
}

I couldn't find the third example on this site, and the thing came up in the Lounge<C++> chat (in discussion with @rightfold, @Abyx and @JerryCoffin), The somewhat surprising thing is that converting the std::string constructor taking a count and a character to use {} instead of (), changes its meaning from n copies of the character to the n-th character (typically from the ASCII table) followed by the other character.

This is not caught by the usual brace prohibition on narrowing conversions, because 65 is a constant expression that can be represented as a char and will retain its original value when converted back to int (§8.5.4/7, bullet 4) (thanks to @JerryCoffin).

Question: are there more examples lurking in the Standard Library where converting a () style constructor to {} style, is greedily matched by an initializer-list constructor?

TemplateRex
  • 69,038
  • 19
  • 164
  • 304
  • 1
    Probably any container would have this issue – aaronman Nov 07 '13 at 22:31
  • A quick search through the standard shows only: `string`, `valarray`, all containers, `min`/`max`/`minmax`, regexps, some random distributions, and `seed_seq`. – Kerrek SB Nov 07 '13 at 22:33
  • 2
    Sorry for the pedantry. "All other things equal, ..." is not quite how it works. The initializer list constructor is selected even if it provides a *much worse* match (for the definition of "worse" that would compare the two constructors as simply two functions with the initlist elements as arguments). The way it works is that the other constructors are simply ignored. – Johannes Schaub - litb Nov 07 '13 at 22:48
  • @JohannesSchaub-litb tnx, updated. – TemplateRex Nov 07 '13 at 22:50
  • I don't even know why `initializer_list` was added to the language; very rarely do you ever construct a container with a compile-time known number of objects. All it's done in my mind is introduce silly edge cases. – Simple Nov 08 '13 at 11:19
  • 4
    There is one exception, default constructors are higher on the priority list than `initializer_list` constructors. So `{}` calls the default constructor, and never a zero-length `initializer_list`. Unless your class lacks a default constructor, then an `initializer_list` constructor with zero entries will be used. (If there are multiple `initializer_list` constructors, you get an ambiguity error.) – CTMacUser Nov 08 '13 at 20:54

2 Answers2

5

I assume, with your examples for std::vector<int> and std::string you meant to also cover the other containers, e.g., std::list<int>, std::deque<int>, etc. which have the same problem, obviously, as std::vector<int>. Likewise, the int isn't the only type as it also applies to char, short, long and their unsigned version (possibly a few other integral types, too).

I think there is also std::valarray<T> but I'm not sure if T is allowed to be integral type. Actually, I think these have different semantics:

std::valarray<double>(0.0, 3);
std::valarray<double>{0.0, 3};

There are a few other standard C++ class templates which take an std::initializer_list<T> as argument but I don't think any of these has an overloaded constructor which would be used when using parenthesis instead of braces.

Dietmar Kühl
  • 150,225
  • 13
  • 225
  • 380
  • @aaronman: thanks. fixed. This would nearly look as if I copy&pasted code! Of course, this wasn't the case I consider duplicating code offensive ;-) – Dietmar Kühl Nov 07 '13 at 22:46
  • 2
    oh great, `std::valarray` has `(val, count)` instead of `(count, val)` like all sequence containers, even more error-prone – TemplateRex Nov 07 '13 at 22:47
  • 2
    @TemplateRex: As far as I can tell, you are making an error the moment you type `std::valarray` into your source. – Dietmar Kühl Nov 07 '13 at 22:49
  • Re: use of valarray, [some people](http://stackoverflow.com/a/16986175/819272) disagree. – TemplateRex Nov 07 '13 at 22:52
  • That answer says "just for fun" ... I would be surprised if he would really use `valarray` in serious code, not just in a 12-line answer on SO. Don't mistake code on SO for the real world! ;-) – Jonathan Wakely Nov 07 '13 at 22:55
  • I would be happy to use `std::valarray` if their implementations were more reliable. Sadly, I have to resort to third party template metaprogramming libraries. It would be much nicer to have a standard solution that is bullet proof. – Ali Nov 07 '13 at 22:55
  • @DietmarKühl btw, `std::set` does not have such a constructor, only sequence containers – TemplateRex Nov 07 '13 at 22:56
  • @JonathanWakely regardless of the intrinsic `valarray` merits, there is also an chicken-and-egg-effect: `valarray` is obscure, so using it is likely to cause misunderstanding, which perpetuates the obscurity. It reminds me of the bad performance of bitfields, which nobody fixes because nobody is using them because of their bad performance... – TemplateRex Nov 07 '13 at 23:00
  • 1
    @TemplateRex: replace `std::set` by `std::deque`: I didn't verify its constructors. Re `std::valarray`: They made into the standard fairly late and the moment the initial version was voted in the people pushing for them vanished. As a result, their interface didn't get corrected although, e.g., David Vandevoorde tried to do so. [Blitz++](http://sourceforge.net/projects/blitz/) showed what was really requested. However, nobody working on the standard had the time and energy to patch it up. `std::valarray` is useful enough to be used but not what is really wanted. – Dietmar Kühl Nov 07 '13 at 23:30
4

Just searching for the occurence of initializer_list.

  • All sequences, they are have the constructors like that of vector:

    • deque
    • dynarray
    • forward_list
    • list
    • vector
  • valarray

  • basic_string

  • Unordered collections, there is a constructor which takes an integer to determine the initial bucket count.

    • unordered_set
    • unordered_multiset

I think that's all of it.

#include <unordered_set>
#include <iostream>

int main() {
    std::unordered_set<int> f (3);
    std::unordered_set<int> g {3};
    std::cout << f.size() << "/" << g.size() << std::endl; // prints 0/1.
}
kennytm
  • 510,854
  • 105
  • 1,084
  • 1,005
  • thanks, that's quite systematic. if you could expand this to conditions on the types and values (so that they are not prevented by the narrowing prohibition), that would make it the accepted answer. – TemplateRex Nov 07 '13 at 23:12