Which greedy initializer-list examples are lurking in the Standard Library?
Asked Answered
K

2

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?

Kendrickkendricks answered 7/11, 2013 at 22:25 Comment(6)
Probably any container would have this issuePleasant
A quick search through the standard shows only: string, valarray, all containers, min/max/minmax, regexps, some random distributions, and seed_seq.Skinned
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.Obligate
@JohannesSchaub-litb tnx, updated.Kendrickkendricks
I don't even know why initializer_list<T> 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.Postmark
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.)Cauley
U
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.

Upsetting answered 7/11, 2013 at 22:43 Comment(9)
@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 ;-)Strabismus
oh great, std::valarray has (val, count) instead of (count, val) like all sequence containers, even more error-proneKendrickkendricks
@TemplateRex: As far as I can tell, you are making an error the moment you type std::valarray into your source.Strabismus
Re: use of valarray, some people disagree.Kendrickkendricks
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! ;-)Edgebone
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.Retard
@DietmarKühl btw, std::set does not have such a constructor, only sequence containersKendrickkendricks
@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...Kendrickkendricks
@TemplateRex: replace std::set<int> by std::deque<int>: I didn't verify its constructors. Re std::valarray<T>: 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++ showed what was really requested. However, nobody working on the standard had the time and energy to patch it up. std::valarray<T> is useful enough to be used but not what is really wanted.Strabismus
B
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.
}
Bohn answered 7/11, 2013 at 22:59 Comment(1)
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.Kendrickkendricks

© 2022 - 2024 — McMap. All rights reserved.