Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Ideally C++ would tell people you can't do that and make it a compiler error ("Ill-formed") but my guess is that too many people insist they ought to be able to take arbitrary C++ 23 code, recompile it with C++ 26 and have that just always work even though the standard doesn't deliver that and so it won't happen.

P2723 is unlikely to happen. The "Erroneous behaviour" P2795 might have a better chance. This would say it's wrong to do uninitialized reads (whereas P2723 says they are initialized to zero and thus it's not wrong) but you always get zero anyway.

I think there's a fair chance WG21 manages to make everybody unhappy by kicking this can into the long grass as they have on many other controversial issues.

Zero is the wrong default, it's better than UB, but it's not good. This is actually a problem in languages like Go where zero defaults are core to the language design. The correct thing is that "I didn't initialize it" won't compile. Force the programmer to write what they meant, sometimes they meant zero, or None, or 0.0 or whatever, but surprisingly often when confronted with the question the programmer realises their design is wrong and needs a design level change.



> Force the programmer to write what they meant, sometimes they meant zero, or None, or 0.0 or whatever, but surprisingly often when confronted with the question the programmer realises their design is wrong and needs a design level change.

I almost sympathize with your point, except we are taking about a language where `Type var;` is the explicit way to initialize many variables to a perfectly well defined value: it is the only way to call the no-arguments constructor for a variable on the stack. It's only for non-class types that this has the bizarre behavior of allocating but not setting any value.

It's even worse in a language with templates:

  template <class T>
  void foo() T {
    T local;
    return local;
  }
Can be perfectly correct OR it can be UB based on the type of T.


> [...] it is the only way to call the no-arguments constructor for a variable on the stack

The syntax:

  T var{};
always value-initializes a stack allocated variable (or a member variable or a global).


I didn't know about the empty initializer list syntax.

Still, reading about it, there are cases where `T var{} ;` will do something different from `T var;`: if T is an aggregate object type, then it will invoke aggregate initialization instead of calling the no-args constructor.


If T is an aggregate, there is, by definition, no constructor (no-argument or otherwise) to call. The aggregate initialization will then recursively value initialize each member, which is what you want.

The only catch is, as usual, list-initialiation. You have to hope that T is sane and any list initialization constructor with an empty list is equivalent to the nullary constructor.


There was one case I found on SO and later reproduced, where a class B derives publically from another class A which has a protected no-args constructor. In that case, B b; is valid, but B b{}; is not, since it tries to construct an instance of A from the calling code itself using the protected constructor, which it's obviously not allowed to access.

Overall I think it's safe to say that the two syntaxes have different semantics, even if they overlap in most cases.


I have to try that. There are defect reports for corner cases.

In any case this is also allowed:

  T val = T();
And copy elision is now guaranteed.


The example looks like this:

  class A {
  protected:
    A() {}
  }

  class B : public A {}

  B x; //ok
  B y{}; //not ok, can't access A::A()
Would the T val = T(); example work if you don't have a copy constructor at all, or no move constructor, or custom ones which do weird things?

Edit: I checked, and you're right - the syntax seems to be fully equivalent in C++17 or later. Great to hear!


Played it a little bit: this seems to be a regression and breaking change from C++14 where B would not be an aggregates, so B{} would just invoke the default constructor. This is probably an oversight, I wonder if there is a Defect Report.

edit: it works by making the inheritance protected, as B is no longer an aggregate. The right fix would be to also disqualify B from aggregate status if the base class constructor is unreachable.

Also making both A and B non-empty removes aggregate status, so it is really a dark corner of the language.


> it is the only way to call the no-arguments constructor for a variable on the stack.

Is that really true? Ouch. In many languages that wouldn't feel crazy, but in a language where there's a whole book about initialization https://leanpub.com/cppinitbook that feels kinda silly.


A sibling response pointed out that adding an empty pair of braces (an ampty initializer list) after the var name can also invoke the no-args constructor, but it can also do other things depending on the class. So yes, I believe this is the only way of explicitly calling the no-args constructor whole in-place on a stack variable.

Ideally the syntax `T var();` would have worked as well, but it turns out that it would be ambiguous with declaring a local function named var that takes no arguments and returns a T...


>The correct thing is that "I didn't initialize it" won't compile

Flawless detection of uninitialized reads would require solving the halting problem, which is impossible. So requiring initialization does prevent optimal efficiency of some theoretical programs. Of course, this would only matter in cases where performance was extremely critical (and the whole point becomes moot if the alternative is to automatically zero the memory, which is even worse in this pedantic optimal-performance sense).


Having some means (as these C++ proposals all do) to explicitly say "I understand that you can't see why this is correct but it assure you it is" would be fine, and needn't be introduced to beginners at all. The problem as usual in C++ is that All The Defaults Are Wrong and because they're defaults we need to warn beginners about them.

You won't write Rust's MaybeUninit<T>::assume_init() in your first program by mistake, whereas the equivalent mistake in C++ happens easily because it's the default.


The question essentially is what the statement `T x;` should mean. Today, if T is class, it means "allocate space for a value of type T and construct it using T::T()". However, if T is a built-in type, it means "allocate space for a value of type T with no defined value", which has proven to be highly problematic in practice.

The situation could be improved in two simple ways. One, you could unify the two meanings, and say that `T x;` allocates space and calls T::T() to initialize the value. The no-args constructor for built-in types already exists and initializes them to 0.

Or, you could also say `T x;` is illegal syntax, one must write `T x = val;` always (or at least when T is a built-in type).

In either case, an escape hatch is needed for allocating uninitialized space on the stack, since there are valid performance reasons for wanting that, in rare cases. But that should be new syntax, it really really shouldn't be the default. So you can still do something like `T x = std::uninitialized();` or whatever the syntax would be to get the current behavior in performance-critical cases, where the tradeoff makes sense.

Personally, especially given C++'s use of templates that don't distinguish between built-in types and classes, I believe the first option makes the most sense, and in fact removes am ugly inconsistency from the language.




Consider applying for YC's Fall 2026 batch! Applications are open till July 27.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: