Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

std::set insert with initialiser lists

I have this simple code:

struct Base
{
    Base(int xx, int yy) : x(xx), y(yy){}
    bool operator<(const Base& b) const {return (x < b.x) || (x==b.x && y < b.y);}

    int x;
    int y;
};

struct D1 : Base
{
    D1(int x, int y) : Base(x, y){}
};

struct D2 : Base
{
    D2(int x = 0, int y = 0) : Base(x, y){}
};

void test()
{
    std::set<D1> s1;
    std::set<D2> s2;

    s1.insert({1, 2});
    s2.insert({1, 2});

    std::cout<<"s1 size:"<<s1.size()<<std::endl<<"Content:"<<std::endl;
    for(auto& v : s1)
    {
        std::cout<<v.x<<" "<<v.y<<std::endl;
    }

    std::cout<<std::endl<<"s2 size:"<<s2.size()<<std::endl<<"Content:"<<std::endl;
    for(auto& v : s2)
    {
        std::cout<<v.x<<" "<<v.y<<std::endl;
    }
}

With the output:

s1 size:1
Content: 
1 2

s2 size:2
Content:
1 0
2 0

Why is the behaviour different when inserting objects with default arguments? Is this a bug or is it the intended behaviour?

PS: You can see the code in action here: https://ideone.com/UPArOi

like image 728
Mircea Ispas Avatar asked Sep 15 '15 13:09

Mircea Ispas


2 Answers

The rule of thumb here is that the initializer_list<X> overloads are strongly preferred to other overloads.

First, from [over.ics.list]

if the parameter type is std::initializer_list<X> and all the elements of the initializer list can be implicitly converted to X, the implicit conversion sequence is the worst conversion necessary to convert an element of the list to X, or if the initializer list has no elements, the identity conversion. This conversion can be a user-defined conversion even in the context of a call to an initializer-list constructor.

And, from [over.ics.rank]:

List-initialization sequence L1 is a better conversion sequence than list-initialization sequence L2 if
— L1 converts to std::initializer_list<X> for some X and L2 does not [...]

We have two relevant overloads of std::set::insert:

std::pair<iterator,bool> insert( value_type&& value );
void insert( std::initializer_list<value_type> ilist );

For the first call:

s1.insert({1, 2});

Consider the overload with parameter type std::initializer_list<D1>. Neither 1 nor 2 can be implicitly converted to D1, so that overload is not viable. Now consider the D1&& overload. Since we can construct a D1 with that initializer list, that's the overload that's selected, and we end up with a single element: D1{1, 2}.

However, in this case:

s2.insert({1, 2});

Both 1 and 2 can be implicitly converted to D2, thanks to the default argument in D2's constructor. So the initializer_list<D2> overload is viable. The D2&& overload is viable as well, but the initializer_list conversion sequence is a better conversion sequence, so it's preferred. This gives us two elements, D2{1} and D2{2}.

like image 163
Barry Avatar answered Oct 14 '22 11:10

Barry


This is standard behavior. It's because you're using an initialization list to insert into your set. What that does for your default args is to create two objects with each int (using the defaulted value for the second arg).

like image 35
Paul Evans Avatar answered Oct 14 '22 10:10

Paul Evans