Skip to content
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

Closed
gdr-at-ms opened this issue Sep 30, 2015 · 27 comments
Closed

Default constructor, move constructor, and noexcept #231

gdr-at-ms opened this issue Sep 30, 2015 · 27 comments
Assignees
Labels

Comments

@gdr-at-ms
Copy link
Contributor

Make default constructors noexcept by default.
Make move constructor noexcept by default.

@gdr-at-ms gdr-at-ms self-assigned this Oct 2, 2015
@gdr-at-ms gdr-at-ms added the open label Oct 2, 2015
@SaintDubious
Copy link

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.

@gdr-at-ms
Copy link
Contributor Author

If the default constructor cannot be noexcept, it is probably doing too much work, for a default constructor. It does not follow that one usually needs is_properly_constructed(). It only means that one should try hard to keep default constructors sufficiently simple. It also does not mean that all default constructors must be noexcept.

@SaintDubious
Copy link

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.

@LegalizeAdulthood
Copy link

@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.

@nadiasvertex
Copy link

@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.

@LegalizeAdulthood
Copy link

@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".

@SaintDubious
Copy link

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.

@galik
Copy link
Contributor

galik commented Jan 8, 2016

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 nullptr to make it usable but it does not need to contain any text or even allocated memory. Same with containers in general.

@SaintDubious
Copy link

@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 noexcept. However that choice is much more nuanced then whether or not we should declare destructors noexcept. The statement in F.6 should be changed to read "Destructors, swap functions, and move operations should never throw."

@gdr-at-ms
Copy link
Contributor Author

@SaintDubious
In what state do you leave your object when you move from it?

@nadiasvertex @galik
Exactly right.

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.

@SaintDubious
Copy link

@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 noexcept). For constructors it depends on the situation. And I don't have enough understanding to have an opinion of move operations.

@gdr-at-ms
Copy link
Contributor Author

@SaintDubious
If you have a rule that says that a swap function must be noexcept (and you say that rule is obvious), how do you go by writing your sawp() function concretely if your constructors are at liberty of throwing? What other requirements you must follow or must you impose on your types to make that happen?

@LegalizeAdulthood
Copy link

@gdr-at-ms I thought the point of writing your own swap was when you had more knowledge of the internal representation to be able to perform the operation more efficiently than using c'tor and temporaries as the generic algorithm does?

@SaintDubious
Copy link

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.

@bengimizrahi
Copy link

I agree. If throwing is not allowed for default constructors, then it implies that a default constructors are not allowed to call operator new()!

@AndrewPardoe
Copy link
Contributor

AndrewPardoe commented Oct 17, 2016

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).

@AndrewPardoe
Copy link
Contributor

Revisited today. We are making progress on this elsewhere and still plan to create the referenced PR.

@ricab
Copy link

ricab commented Mar 8, 2018

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 init when needed.

@gdr-at-ms

In what state do you leave your object when you move from it?

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 noexcept so badly needed to make it a guideline?

@jwakely
Copy link
Contributor

jwakely commented Mar 8, 2018

In a state that is just good enough for destruction.

That's not very good practice. It's not what the standard library types do.

Why is noexcept so badly needed to make it a guideline?

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.

@blakehawkins
Copy link
Member

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 init when needed.

FWIW this is still a design I strive for - but I achieve it by using private constructors and a public factory method that returns std::optional<T>

@MikeGitb
Copy link

MikeGitb commented Mar 9, 2018

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 [[gsl::move_invalidates_object]] attribute.

@ricab
Copy link

ricab commented Mar 9, 2018

@jwakely

That's not very good practice.

Not in general, but in many cases I think it is. Namely in types that, precisely in order to maintain an invariant, either:

  • do not have a default constructor, or
  • have a default ctor that needs to do complex work (~= the case discussed in this issue).

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:

  1. its domain needs to be artificially augmented to include a special (i.e. empty) state, along with corresponding checkers (e.g. is_empty)
  2. it leaves moved-from objects in one of the normal valid states
  3. it disallows move operations
  4. it establishes not moved-from as a pre-condition to all operations except those strictly necessary for destruction and recycling (dtor, assignment)

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 = delete). That means that, when choosing this option, some perfectly valid C++98 classes have to be modified for C++11. For instance, in C++98, the implementation of Foo below conforms to a specification requiring a valid-Bar invariant, but in C++11, move operations would have to be deleted for that.

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.

@kylereedmsft
Copy link

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).

@vmgolovin
Copy link

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.
In my opinion, only these operations should never throw: destructor, swapping and move assignment (not constructor), because it always can be implemented via swapping (is it efficient or not).

@vmgolovin
Copy link

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
Actually, this has already been stated in this thread, @ricab gave a good summary.

@hsutter hsutter closed this as completed Jul 30, 2020
@hsutter
Copy link
Contributor

hsutter commented Jul 30, 2020

Editors call: This is now already covered.

@ricab
Copy link

ricab commented Jul 31, 2020

Long story short: move semantics broke RAII (resource lifetime tied to object lifetime).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests