Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
What to do if you don't want a default constructor? (sandordargo.com)
57 points by jandeboevrie on July 20, 2024 | hide | past | favorite | 14 comments


It’s nice to make invalid states unrepresentable. Unfortunately, you can’t do that in C++ if you have a move constructor. C++ moves have to leave the source object in a valid state, since the object can still be accessed afterwards and will still have its destructor run. But unless your move is really just a copy, that state has to be some kind of default, or at least semantically meaningless. And if a valid but semantically meaningless state exists, arguably you might as well use it for the default constructor too.


I don’t think that argument holds much water.

A moved-from object needs to be in a valid state because the destructor will still run, and so that needs to succeed (and not UB).

Nobody ever says the moved-from state had to be useful or recoverable from.

Alternatively, you can remove support for moving. That way the moved-from issue disappears.


The standard library types are guaranteed to be in a useful state after being moved from (the term "valid state" is used for this). Of course, that doesn't mean that your own types have to, but the C++ Core Guidelines suggest doing so [1].

1: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines...


I'm not sure I would say that "valid" and "useful" are the same. Things that must be safe after moving: destructing, assigning. No other operation need make sense.

It sucks that C++ can't enforce "don't use struct after moving". It's definitely a wart, of which C++ has no shortage. But you can still get away with no default constructor and not having to sprinkle a kajillion "isUseful" checks around your codebase.

Code can assume the struct is useful. The case where it's not is rare and probably won't ever come up. Such is life when writing C++!


> I'm not sure I would say that "valid" and "useful" are the same.

In standardese, they are: an object in a "valid but unspecified state" requires "that the object’s invariants are met and operations on the object behave as specified for its type"[1]. In other words, any operation without preconditions may be performed.

> Things that must be safe after moving: destructing, assigning. No other operation need make sense.

That's not accurate for standard library types: moved-from objects must be in a "valid but unspecified state"[2].

1: C++23 working draft, 3.66 valid but unspecified state [defns.valid]

2: C++23 working draft, 16.4.6.15 Moved-from state of library types [lib.types.movedfrom]


You make it seem like such a terrible thing, but a valueless state is the only exception to "you can make invalid states unrepresentable", and a pretty useful one given the rest of the language. And it's not one that you have to worry about in practice because nobody will intentionally abuse this, and your compiler/linter will diagnose accidental reuses. Guarding against it anyway is like guarding against someone passing a destroyed object to you - it just doesn't make sense to.


> Guarding against it anyway is like guarding against someone passing a destroyed object to you - it just doesn't make sense to.

It doesn’t make sense to do so intentionally, but that’s a classic dangling-pointer/use-after-free bug. In some contexts it might make sense to guard against it for the same reasons one may validate arguments passed to a public API (and then not in the internal code)


I've never liked default constructors when it produces something that is not representable without executing code. D does not have default constructors. What one does is specify the statically initialized field values (or leave the fields to be set to their statically initialized value). I.e. it has a default initializer.

What this means in practice is one never is presented with an uninitialized or partially initialized struct. A non-default constructor gets handed a default initialized struct to start with. This makes for more reliable software. (Double initialization can be removed by the optimizer.)

Why D doesn't have default constructors gets brought up now and then, as it is an unusual choice.


A pattern I've seen some people do is to just make an operator bool for the type that checks for a contextually-sensical invalid state that is also the default constructed state. This is absolutely a hack, and has lots of other implications, and I wouldn't recommend it, but I see why people do it, because it creates a low-overhead way of getting around the possibility of invalid stuff while not taking on the problems of lacking a default constructor

Personally, I like the simplicity of this kind of approach, but prefer to force consumers of the type to be more explicit. To that end, I really like having an constexpr bool operator! instead, so the same checks can be performed without making it easy to accidentally coerce your type into some shady arithmetic operations. You still can if you meant to, but it will be more obvious what you're doing and won't happen accidentally


I’m not sure I follow entirely, but an explicit operator bool will only auto-convert in contexts where it makes sense, and not e.g. in arithemtic expressions.

https://stackoverflow.com/questions/39995573/when-can-i-use-...


Explicit keyword seems reasonable too, I just like !! more than the verbosity of casting tbh. Totally aesthetic preference


operator<=> was new to me. It's the three-way comparison operator.

https://en.cppreference.com/w/cpp/language/operator_comparis...


aka the spaceship operator. By defining just the spaceship operator for some types A and B, the compiler can synthesize overloads for the typical comparison operators like < <= == >= > for A vs B and B vs A.


-O7 ?




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

Search: