Search code examples
c++initializer-list

Comparison of object against {}


Default-constructed objects often represent an "initialized, but no value stored" state. For example, a default-constructed std::unique_ptr does not point to anything (and in practice represents this by storing a the exceptional value nullptr).

On the other hand, default initialization is often done by assigning {}. This would suggest that one might want to test whether an object is default-initialized by comparing against {}, but the following code does not compile:

struct S
{
  S() = default;
  bool operator== (const S&) const { return false; }
};

int main ()
{
  S s = {};     // Default initialization
  s = {};       // Assigning a default-initialized object

  if (s == {})  // Check if 's' contains a value
    ;
}

The error one gets is this:

x.cc:12:12: error: expected primary-expression before ‘{’ token
   12 |   if (s == {})  // Check if 's' contains a value
      |            ^

Demo

(One might be tempted to add bool operator== (const std::initializer_list<int>()); to the class, but that too does not help.)

My questions then are:

  1. Why is it that in a statement s = {}, the right hand side is correctly converted to a default-constructed object, but in s == {} it is not? (Clearly, using s == S{} works -- but that's not the question.)
  2. Why does the addition of bool operator== (const std::initializer_list<int>()); not help?
  3. Is there an overall rationale (other than "because the standard does not allow it") why allowing this kind of syntax to test for an object being empty is undesirable?

Solution

  • S s = {}; // Default initialization

    That's not default-initialization. That's copy-list-initialization with empty initializer list. For a non-aggregate class with default constructor (as here is the case), that resolves to value-initialization, which is again a different form of initialization than default-initialization, although both will call the default constructor (in this case). Initialization rules of C++ are very complex and without using correct terminology trying to follow them is a lost cause.

    s = {}; // Assigning a default-initialized object

    That's not what the syntax means. The syntax just means that overload resolution for the = operator is done with {} as argument.

    So it is (almost) equivalent to

    s.operator=({});
    

    Depending on what kind of operator= overloads s has, this can have any kind of meaning. For example there may only be a single overload with signature operator=(int), in which case this would be equivalent to s = 0;.

    However, in most cases a class has only copy/move assignment operators. For example S has an implicit move constructor

    operator=(S&&)
    

    Overload resolution will choose this one (the copy assignment is a worse match). When initializing a S&& reference from empty braces {}, this is equivalent to creating a temporary object of type S and initializing it as if by = {} (i.e. copy-list-initialization with empty initializer list as above). The reference in the assignment operator then binds to this temporary object.


    The {} in s = {} is a bit strange, because (in contrast to = {} in initialization) s = {} is an expression and normally the operands of an expression are also expressions. But {} is not an expression. It is its own syntax construct without type and value category which expressions should have.

    Braced-init-lists such as {} are permitted as operands instead of expressions only for the right-hand side of = and for function call arguments. In either case, they do not behave like normal expressions would in their place and require special rules to handle.

    It would be possible to add similar syntax constructs for other operators so that they also do overload resolution with the braced-init-list as argument, but that was simply never added to the language. Either it wasn't proposed by anyone to the standard committee or it was proposed and didn't pass through the process to approval and inclusion in the standard.

    The behavior was added with C++11 for = and function call arguments only so that braces could be used universally to "initialize" or set the value of objects. (Although that never really turned out to work as intended.) Prior to C++11 braces could only initialize aggregate types and non-aggregate classes had to be initialized with parentheses instead.

    Whether to allow braced-init-lists as more expression operands has been discussed in the contexts of introduction of these C++11 additions. Some rationale against them is referenced in this answer.