-
Notifications
You must be signed in to change notification settings - Fork 5.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Default constructor, move constructor, and noexcept #231
Comments
|
I was going to bring this up as a separate issue, but I think this is an appropriate place to discuss. F.6 says that default constructors should be noexcept. But C.42 says that if a constructor fails it should throw an exception. Personally I'm in the camp that any constructor should feel free to throw, in fact it's the only way to signal an error in a constructor. If you take that away then you're left with having to have some kind of bool is_properly_constructed() function that you have to call after the constructor finishes. I'd suggest that default constructor be removed from the list of things that are made nodexcept. |
|
If the default constructor cannot be |
|
The statement in F.6 reads "Destructors, swap functions, move operations, and default constructors should never throw." There are very good reasons why destructors and swap should not throw. Putting default constructors in this same class is misleading. I'm not aware of any reason why any constructor should not throw, in fact C.42 explicitly says constructors SHOULD throw. I also don't think that default constructors are expected to be simpler then other constructors. |
|
@SaintDubious I concur. For instance, the default constructor for some remote resource might assume a default host name and port for establishing a TCP/IP connection. If that remote service is unreachable, then the default constructor could throw. The non-default constructor might let you specify a specific host name and port, but wouldn't be intrinsically any more complicated than the default constructor. |
|
@LegalizeAdulthood I think the point is that your constructor probably should not be connecting to anything. Maybe you have a constructor which has a "connect automatically" param. Maybe there is also a "connect" method. This would make sense because there might be a lot of configuration you want to do around the connection. I think similar arguments could be made for any situation where a default constructor could throw. The default constructor should just setup sane values for the members, and then wait for further instruction. |
|
@nadiasvertex In my experience, when that approach has been used in a code base, then the default constructed object is basically worthless and the useful initialization is always performed in a second method. In other words, this object represents a remote resource. To default construct it, but not have it connected to a remote resource, means that this object is both not a remote resource and a remote resource. We can nit pick the specifics, but it's not the specifics that I'm discussing. It doesn't make any sense to have an object that represents a "thing" and yet when it is default constructed, it doesn't actually represent a "thing". |
|
I agree with @LegalizeAdulthood on this one. Regardless of which constructor is used, a constructed object should be a fully initialized object. In fact this is the exact point of C.41. |
|
I'm a minimalist when it comes to construction. For me "fully initialized" means putting the object into the minimal state required for it to function without UB. Everything beyond that is configuration which may be appropriate for non-default constructors but I usually leave that to member functions. For example a string class may need its internal pointers setting to |
|
@galik I agree that in many cases it's correct for a constructor (default or not) to do only the bare minimum. And in those cases I have no issue with declaring them |
|
@SaintDubious @nadiasvertex @galik All: there might be something in the various messages to distil as wisdom, but we can't get there if everything is caricatured. Clearly, it is possible to properly design most standard containers (for example) such that the default constructor is well-defined but is also noexcept. This actually matches my experience across a wide range of projects. |
|
@gdr-at-ms I've actually been trying to avoid the question of move operations because I'm only just learning about them. Maybe the statement in F.6 should just say "Destructors and swap functions should never throw?" In summary, I think the guidance for destructors and swap functions is obvious (always |
|
@SaintDubious |
|
@gdr-at-ms I thought the point of writing your own |
|
Nothing new to add, but I wanted to make sure that "C.44: Prefer default constructors to be simple and non-throwing" is included in the discussion. |
|
I agree. If throwing is not allowed for default constructors, then it implies that a default constructors are not allowed to call operator new()! |
|
Gaby, please create a PR for this. Also please take a look at the move assignment items when you write this up (C.62 and following). |
|
Revisited today. We are making progress on this elsewhere and still plan to create the referenced PR. |
|
I thought the point for ctors was to guarantee existing objects are ready for use, thus avoiding artificial intermediate states leaking into the problem domain. Otherwise we might as well use C structs and call
In a state that is just good enough for destruction. Basically garbage awaiting removal. Making the default ctor put the obj in a similar state, when it could properly initialized, does not sound great. I'd prefer not providing a default c'tor at all. What am I missing? Why is |
That's not very good practice. It's not what the standard library types do.
Because if you can't have a noexcept default constructor, you either can't have a noexcept move constructor, or you leave moved-from objects in a dangerous, error-prone state. Both are bad. |
FWIW this is still a design I strive for - but I achieve it by using private constructors and a public factory method that returns |
|
A bit OT, but actually, I like the Idea of leaving an object In a "barely destructible" state after move if it has no obvious empty state. It would just require a little help from the static analyzer and something like |
Not in general, but in many cases I think it is. Namely in types that, precisely in order to maintain an invariant, either:
IOW, types representing problem domains that do not include empty states. A couple of examples: the connection above; a type involving an owning pointer, like the pimpl here. Taking this and this into account, I see 4 options for such a type to comply with the rule determining that a moved-from object must be in a valid but unspecified state:
Option 1. leaks technical details into the problem domain (a new "value" to consider that is only needed for purely technical reasons). In other words, it breaks what could otherwise be a helpful invariant (object in non-empty state). Option 2. would amount to a copy or worse. What valid connection should be left? Either the previous one, or a new one. Both defeat the purpose of moving in the first place, which leads to... Option 3. This is a possibility, but it means systematically discarding a potential performance benefit and, crucially, it may have to be done manually (e.g. via class Foo // respects invariant "bar is valid" in C++98; not in C++11 if we demand ideal moves
{
public:
Foo() : m_bar{complex_action_to_obtain_a_valid_bar()} {}
private:
my_cpp98_smart_ptr<Bar> m_bar;
};Option 4. will often be preferable, especially in high-level tasks, with heavy non-library objects for which copy and move are meaningful operations (e.g. some game building block). But option 4. basically equates to having garbage waiting to be destroyed. |
|
This is a very good discussion and I've gone from "this is a bad guideline" to "waffling" after reading through the thread. The intention behind this needs to be explained in more detail in the guidelines, especially considering that it does seem to conflict with other rules (i.e. C.42). |
|
It seems to me that the authors was trying to sit on two chairs at once when creating the guidelines C.40-C.42+C.64 and C.66+C.45. These two sets of guidelines contradict each other, as for me: the first one discourages the notorius Stepanov's partially-formed state, but the second one kinda requires it in most cases. |
|
Also, Sean Parent has an opinion about moving that goes against the C.64 guideline, namely he embraces the partially-formed state because in many cases this is the only efficient way: https://sean-parent.stlab.cc/2014/05/30/about-move.html |
|
Editors call: This is now already covered. |
|
Long story short: move semantics broke RAII (resource lifetime tied to object lifetime). |
Make default constructors noexcept by default.
Make move constructor noexcept by default.
The text was updated successfully, but these errors were encountered: