Overload Resolution: Assignment of Empty Braces

Overload resolution: assignment of empty braces

Why is operator=(int) selected here, instead of "ambiguous" or the other one?

{} to int is the identity conversion ([over.ics.list]/9). {} to S is a user-defined conversion ([over.ics.list]/6) (technically, it's {} to const S&, and goes through [over.ics.list]/8 and [over.ics.ref] first before coming back to [over.ics.list]/6).

The first wins.

Is there a tidy workaround?

A variation of the trick std::experimental::optional pulls to make t = {} always make t empty.
The key is to make operator=(int) a template. If you want to accept int and only int, then it becomes

template<class Int, std::enable_if_t<std::is_same<Int, int>{}, int> = 0>
S& operator=(Int t) { a = t; return *this; }

Different constraints can be used if you want to enable conversions (you'd probably also want to take the argument by reference in that case).

The point is that by making the right operand's type a template parameter, you block t = {} from using this overload - because {} is a non-deduced context.

...without changing S?

Does template<class T> T default_constructed_instance_of(const T&) { return {}; } and then s = default_constructed_instance_of(s);count?

Overload resolution with an empty brace initializer: pointer or reference?

Oh, this is nasty.

Per [over.ics.list]p4 and p7:

4 Otherwise, if the parameter is a non-aggregate class X and overload resolution per 13.3.1.7 chooses a single best constructor of X to perform the initialization of an object of type X from the argument initializer list, the implicit conversion sequence is a user-defined conversion sequence with the second standard conversion sequence an identity conversion. [...]

[...]

6 Otherwise, if the parameter is a reference, see 13.3.3.1.4. [Note: The rules in this section will apply for initializing the underlying temporary for the reference. -- end note] [...]

[...]

7 Otherwise, if the parameter type is not a class:

[...]

(7.2) -- if the initializer list has no elements, the implicit conversion sequence is the identity conversion. [...]

The construction of a const std::pair<int,int> temporary from {} is considered a user-defined conversion. The construction of a const std::pair<int,int> * prvalue, or a const int * prvalue, or a const int temporary object are all considered standard conversions.

Standard conversions are preferred over user-defined conversions.

Your own find of CWG issue 1536 is relevant, but mostly for language lawyers. It's a gap in the wording, where the standard doesn't really say what happens for initialisation of a reference parameter from {}, since {} is not an expression. It's not what makes the one call ambiguous and the other not though, and implementations are managing to apply common sense here.

assignment to type and empty braces. clarification on syntax

In you constructor you are defining a constructor that takes an integer and the default constructor. Remember that a constructor that provides default arguments for all of its parameters defines also the default constructor.

  • You can write it this way:

    A (int a = 0); // a is a default parameter.

In your example:

A (int a = int {});//

The parameter a is a default parameter initialized (not assigned) from a temporary integer which is value-initialized so because it is integer then it is value-initialized to 0 and then is used to initialize the parameter a. (a is a copy of it). Compiler optimize the code to remove the copy in many scenarios.

A (int a = int {5.6});// error
A (int a = int(5.6));// truncated to 5

Compiler variance for ambiguous copy-assignment from empty-braces

Why is nullopt_t required to be DefaultConstructible in the first place?

The spec requirement that nullopt_t shall not be DefaultConstructible is arguably, in retrospect, a mistake based on some LWG and CWG confusion around tag types, and the resolution of this confusion which came only after std::optional was brought in from the Library Fundamentals TS Components.

First of all, the current (C++17, C++20) spec of nullopt_t, [optional.nullopt]/2, requires [emphasis mine]:

Type nullopt_­t shall not have a default constructor or an initializer-list constructor, and shall not be an aggregate.

and its main use is described in the previous section, [optional.nullopt]/1:

[...] In particular, optional<T> has a constructor with nullopt_­t as a single argument; this indicates that an optional object not containing a value shall be constructed.

Now, P0032R3 (Homogeneous interface for variant, any and optional), one of the papers which was part of introducing std::optional, has a discussion around nullopt_t, tag types in general, and the DefaultConstructible requirement [emphasis mine]:

No default constructible

While adapting optional<T> to the new in_place_t type we found
that we cannot anymore use in_place_t{}. The authors don't consider
this a big limitation as the user can use in_place instead. It needs
to be noted that this is in line with the behavior of nullopt_t as
nullopt_t{} fails as no default constructible. However nullptr_t{}
seems to be well formed.

Not assignable from {}

After a deeper analysis we found also that the old in_place_t
supported in_place_t t = {};. The authors don't consider this a big limitation as we don't expect that a lot of users could use this and the user can use
in_place instead.

in_place_t t;
t = in_place;

It needs to be noted that this is in line with the behavior of
nullopt_t as the following compile fails.

nullopt_t t = {}; // compile fails

However nullptr_t seems to be support it.

nullptr_t t = {}; // compile pass

To re-enforce this design, there is an pending issue 2510-Tag types should not be DefaultConstructible Core issue 2510.

And indeed, the initial proposed resolution of LWG Core Issue 2510 was to require all tag types to not be DefaultConstructible [emphasis mine]:

(LWG) 2510. Tag types should not be DefaultConstructible

[...]

Previous resolution [SUPERSEDED]:

[...] Add a new paragraph after 20.2 [utility]/2 (following the header synopsis):

  • -?- Type piecewise_construct_t shall not have a default constructor. It shall be a literal type. Constant piecewise_construct shall be initialized with an argument of literal type.

This resolution was superseded, however, as there were overlap with CWG Core Issue 1518, which was eventually resolved in a way that did not require tag types to not be DefaultConstructible, as explicit would suffice [emphasis mine]:

(CWG) 1518. Explicit default constructors and copy-list-initialization

[...]

Additional note, October, 2015:

It has been suggested that the resolution of issue 1630 went too far in allowing use of explicit constructors for default initialization, and that default initialization should be considered to model copy initialization instead. The resolution of this issue would provide an opportunity to adjust that.

Proposed resolution (October, 2015):

Change 12.2.2.4 [over.match.ctor] paragraph 1 as follows:

[...] For direct-initialization or default-initialization, the candidate functions are all the constructors of the class of the object being initialized. [...]

as long as explicit also implied that the type was not an aggregate, which in turn was the final resolution of LWG Core Issue 2510 (based on the final resolution of CWG Core Issue 1518)

(LWG) 2510. Tag types should not be DefaultConstructible

[...]

Proposed resolution:

[...] In 20.2 [utility]/2, change the header synopsis:


  • // 20.3.5, pair piecewise construction
    struct piecewise_construct_t { explicit piecewise_construct_t() = default; };
    constexpr piecewise_construct_t piecewise_construct{};

[...]

These latter changes, however, were not brought into the proposal for std::optional, arguably an oversight, and I would like to claim that nullopt_t need not be required to not be DefaultConstructible, only, like other tag types, that it should have a user-declared explicit constructor, which bans it from a candidate for empty-braces copy-list-init both by it not being an aggregate and by the only candidate constructor being explicit.

Which compiler is right and wrong here?

Given the LWG 2510, CWG 1518 (and other) confusion, let's focus on C++17 and beyond. In this case, GCC is arguably wrong to reject the program, whereas Clang and MSVC are correct to accept it.

Why?

Because the S& operator=(nullopt_t) assignment operator is not viable for the assignment s = {};, as the empty braces {} would require either aggregate initialization or copy-list-initialization to create a nullopt_t (temporary) object. nullopt_t, however (by the idiomatic tag implementation: my implementation above), as per as per P0398R0 (which resolves CWG Core Issue 1518), is neither an aggregate nor does its default constructor participate in copy-list-initialization (from empty braces).

This likely falls under the following GCC bug report:

  • Bug 54835 - (C++11)(DR 1518) Explicit default constructors not respected during copy-list-initialization

which was listed as SUSPENDED in 2015-06-15, before the change in the resolution of CWG Core Issue 1630 ("resolution of issue 1630 went too far"). The ticket is now re-opened based on a ping from this Q&A.

Why {} is better candidate to be int than string for C++ overload resolution?

From over.ics.list#9.2:

if the initializer list has no elements, the implicit conversion sequence is the identity conversion. [ Example:

 void f(int);

f( { } ); // OK: identity conversion

 — end example ]

Thus, the conversion from {} to int is an identity conversion, while {} to const std::string& is a user-defined conversion. And since the identity conversion is a better match, the overload corresponding to int will be chosen.

Zero-reinitializing a struct in C++

What you are looking for is in [expr.ass]

A braced-init-list may appear on the right-hand side of

  • an assignment to a scalar, in which case the initializer list shall have at most a single element. The
    meaning of x={v}, where T is the scalar type of the expression x, is that of x=T{v}. The meaning of
    x={} is x=T{}.
  • an assignment to an object of class type, in which case the initializer list is passed as the argument to
    the assignment operator function selected by overload resolution (13.5.3, 13.3).

So your guess is correct. The compiler may (can in C++17 and above) be able to optimize things away but you can think of it as create a zero-initialized temporary and pass it to the operator=.

Is assign with braces the same as call the constructor?

This is direct list initialization.

shared_ptr<int> myIntSmartPtr { my_alloc(42), my_free };

This is an example of the first syntax:

T object { arg1, arg2, ... };   (1)

The exact effect it has is therefore

List initialization is performed in the following situations:

  • direct-list-initialization (both explicit and non-explicit constructors are considered)
    1. initialization of a named variable with a braced-init-list (that is, a possibly empty brace-enclosed list of expressions or nested braced-init-lists)

And for more detail about what that actually means:

The effects of list-initialization of an object of type T are:

... [A bunch of cases that don't apply]

Otherwise, the constructors of T are considered, in two phases:

  • All constructors that take std::initializer_list as the only argument, or as the first argument if the remaining arguments have default values, are examined, and matched by overload resolution against a single argument of type std::initializer_list
  • If the previous stage does not produce a match, all constructors of T participate in overload resolution against the set of arguments that consists of the elements of the braced-init-list, with the restriction that only non-narrowing conversions are allowed. If this stage produces an explicit constructor as the best match for a copy-list-initialization, compilation fails (note, in simple copy-initialization, explicit constructors are not considered at all).

std::shared_ptr does not have a constructor that takes an std::initializer_list, so the second bullet point applies and it's constructed from the arguments therein.

What is the purpose of having an empty pair base class?

tl;dr This is the result of a really long series of hacks to implement the insane overload/explicit rules of std::pair and maintain ABI compatibility. It is a bug in C++20.



Disclaimer

This is more of a "fun" ride along with the standard library authors down memory lane then some insightful language level revelation. It shows how extremely complicated C++ has became that implementing a pair, of all things, is a herculean task.

I tried my best recreating the history, but I'm not one of the authors.

Pair Primer

std::pair is much more than simply

template<typename T, typename U>
struct pair
{
T first;
U second;
};

There are 8 different constructors listed on cppreference, and for an implementer, it's even more: every conditionally explicit constructor is actually two constructors, one for implicit, another for explicit.

Not all of these constructors participate in overload resolution, if they did, there would be ambiguity everywhere. Instead, there are many many rules governing when each does, and every combination of the aforementioned cases have to be written and disabled manually by SFINAE.

This culminated to 5 bug reports throughout the years on the constructors alone. Now about to become 6 ;)

Prologue

The first bug is about short-circuiting the checks of convertibility of the pair parameters if the types are the same.

template<typename T> struct B;
template<typename T> struct A
{
A(A&&) = default;
A(const B<T> &);
};

template<typename T> struct B
{
pair<A<T>, int> a;
B(B&&) = default;
};

Apparently, if they checked convertibility too early, the move constructor gets deleted due to the circular dependency and how B is still incomplete within A.

nonesuch

This however changed the SFINAE properties of pair. In response, another fix was implemented. This implementation enabled previously invalid assignment operators, and so the assignment operators were turned off manually by changing their signatures

struct nonesuch
{
nonesuch() = delete;
~nonesuch() = delete;
nonesuch(nonesuch const&) = delete;
void operator=(nonesuch const&) = delete;
};

// ...
pair& operator=(
conditional_t<conjunction_v<is_copy_assignable<T>,
is_copy_assignable<U>>,
const pair&, const nonesuch&>::type)

Where nonesuch is a dummy type that essentially makes this overload uncallable. Or is it?

no_braces_nonesuch

Unfortunately, even though you couldn't ever create a nonesuch

pair<int, int> p = {};  // succeeds
p = {}; // fails

you could still initialize it with braces. Since delete doesn't resolve overload resolution, this is a hard failure.

The fix was to create no_braces_nonesuch

struct no_braces_nonesuch : nonesuch
{
explicit no_braces_nonesuch(const no_braces_nonesuch&) = delete;
};

The explicit turns off participation in overload resolution. Finally, the assignment is uncallable. Or is it...?

__pair_base v1

There is, unfortunately, another way to initialize an unknown type

struct anything
{
template<typename T>
operator T() { return {}; }
};

anything a;
pair<int, int> p;
p = a;

The authors realized they could solve this "easily" by leveraging the default generated special member functions: they could be not declared at all if you have a base that is non-assignable

class __pair_base
{
template<typename T, typename U> friend struct pair;
__pair_base() = default;
~__pair_base() = default;
__pair_base(const __pair_base&) = default;
__pair_base& operator=(const __pair_base&) = delete;
};

All unit tests passed, and things are looking bright. Unbeknownst, the shadow of an evil bug looms ominously on the horizon.

__pair_base v2

ABI broke.

How is that even remotely possible? Empty bases are optimized out aren't they? Well, no.

pair<pair<int, int>, int> p;

Unfortunately, empty base optimization only applies if the base class subobjects are non-overlapping with other subobjects of the same type. In this case, the __pair_base of the inner pair overlaps with the one of the outer pair.

The fix was "simple", we templatize __pair_base to ensure they are different types.

Structural Types

C++20 came, and it requires that pair be structural types. This requires that there is no private bases.

template<pair<int, int>>
struct S; // fails

So ends our journey. This reminds me of Chandler Carruth's quick survey at cppcon: "who can build a C++ compiler in a year if they needed to?" Only current compiler writers think they could, given how complicated C++ is. Apparently, I don't even know how to implement std::pair.

How to maintain initialization of struct as members are added?

struct EnableFlags {
bool doThingA;
bool doThingB;
bool doThingC;
bool doThingD;
// etc for several more bools

void clear() {
*this = EnableFlags();
}
};

This will create a temporary with all members set to zero and then make *this a copy of it. So it sets all the members to zero, no matter how many there are.

This assumes that you haven't defined a default constructor that does something other than set all the flags to false. If you have no user-defined constructors then that assumption holds.

Since C++11 it's even simpler:

void clear() {
*this = {};
}


Related Topics



Leave a reply



Submit