c++stlinitializer-listc++23stdstack

Why do std::flat_set and std::flat_map have overloaded constructors for std::initializer_list while other container adapters don't?


I noticed that C++23 added new overloads for the std::stack and std::queue container adapters' constructors, which allow to construct the underlying container with the contents of the range [first, last). cppreference also shows how these overloads can be used with std::initializer_list, and provides the following examples:

const auto il = {2, 7, 1, 8, 2};
std::stack<int> c4 { il.begin(), il.end() }; // overloads (6), C++23

const auto il = {2, 7, 1, 8, 2};
std::queue<int> c4 { il.begin(), il.end() }; // overload (6), C++23

This means that introducing other overloaded constructors to construct the underlying container with the contents of an std::initialization_list is not necessary. However, C++23 had added also other container adapters, such as std::flat_set, and cppreference shows an example of implementation of the std::flat_set container adapter, where the following overloaded constructors are provided:

flat_set(initializer_list<key_type> il, const key_compare& comp = key_compare())
 : flat_set(il.begin(), il.end(), comp) { }

template<class Allocator>
flat_set(initializer_list<key_type> il, const key_compare& comp, const Allocator& a);

template<class Allocator>
flat_set(initializer_list<key_type> il, const Allocator& a);

Why do std::flat_set and std::flat_map have overloaded constructors for std::initializer_list while other container adapters don't?


Solution

  • I'll copy my std-proposals answer here, since it covers a bit more than LoS's own answer above.

    First, a quotation from P2447R3:

    Yes, any change to overload sets (particularly the addition of new non-explicit constructors) can break code. But that's not necessarily a proposal-killer. For example, there was nothing wrong with C++23’s adopting [P1425] "Iterator-pair constructors for stack and queue" with no change to Annex C, despite its breaking code like this:

    void zero(queue<int>);
    void zero(pair<int*,int*>);
    int a[10];
    void test() { zero({a, a+10}); }
    

    Before: Calls zero(pair<int, int>).
    After P1425: Ambiguous.
    To fix: Eliminate the ambiguous overloading, or cast the argument to pair.

    We can simply agree that such examples are sufficiently unlikely in practice, and sufficiently easy to fix, that the benefits of the changed overload set outweigh the costs of running into these examples.

    Then I wrote:

    I think priority_queue should get an initializer_list ctor, because we all know what priority_queue<int> pq = {1,2,3} ought to do. I think queue probably should get an initializer_list ctor, because I assume we all know what queue<int> q = {1,2,3} ought to do: items pop from the front of the queue, so "1" would be at the front, right? I'm more skeptical of stack. I don't think anyone would guess better than 50/50 what stack<int> st = {1,2,3} ought to do, as written. Items pop from the "top" of a stack, yes, but is that the left end or the right end? (Experts know it must be the right end because that's the only efficient end when the container is a vector; but I don't think that's terribly obvious.) However, on the other hand, it's true that the iterator-pair ctor Does The Right Thing: if you push 1, then 2, then 3, you end up with an underlying vector containing {1,2,3}. So why not just let the programmer write {1,2,3} in the first place? So I'm skeptical, but not completely anti.

    Anyway, in all of those cases, adding new ctors will change overload sets — and change them drastically, because initializer_list ctors are even greedier than other non-explicit ctors. (This is why implicit conversions are the devil, and the STL's prevailing style of "make everything implicit unless there's a positive reason to make it explicit" is the Wrong Default as usual. Python got it right.) So that's probably why LEWG has been leery of doing so.

    OTOH, flat_set and flat_map are completely novel class types; nobody has any existing code that would be broken by fiddling with their overload sets. And flat_set is supposed to be a drop-in replacement for set! So obviously it would be a non-starter if you could write

    std::set<int> s = {1,2,3};
    

    but not

    std::flat_set<int> s = {1,2,3};
    

    That just has to work, period.