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
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 toX
, the implicit conversion sequence is the worst conversion necessary to convert an element of the list toX
, 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 tostd::initializer_list<X>
for someX
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}
.
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With