-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Remove default empty state of sf::Event
#2992
base: master
Are you sure you want to change the base?
Conversation
I considered this in the past and decided I didn’t like it because it requires excessive |
Here's my branch from back in November. I looked at this and didn't think it was worth doing. To avoid lots of |
Pull Request Test Coverage Report for Build 9465514141Details
💛 - Coveralls |
What are the advantages of this over std::monostate? |
Monostate is just another symbol for ab empty state, which is a state a valid event should never be in. As @ChrisThrasher mentioned in his PR, event shouldn't even have an empty state and most importantly such a state should not be public and accessible to users. The reason why we had an empty state to begin with is because some APIa such as pollEvent need to return immediately and tell the user that no event was polled. In C++17 we have an idiomatic way of expressing the possible absence of a result: optional. We are using that throughout the library already and using it for events solves the "public empty state but not really" issue and, even better, makes event a type that's always guaranteed to be valid and non empty. |
@ChrisThrasher Can you explain the rationale behind this? What is wrong with |
It's a meaningless implementation detail. Operating systems or windowing systems do not produce empty events. We simply needed a sentinel to signal "hey, you tried to poll events but nothing was available" in user loops. |
d9de771
to
2953f26
Compare
2953f26
to
887eff1
Compare
sf::Event
887eff1
to
695c250
Compare
@eXpl0it3r @binary1248: could you please express your thoughts on this PR? It follows exactly the same philosophy as these already-merged PRs:
And the same philosophy as the soon-to-be-merged PRs: In fact, I don't understand @ChrisThrasher's concerns given the fact that he's the author of all the PRs mentioned above. It is true that the user experience gets a tiny bit worse due to the fact that the Our users' will benefit from the type safety of There are a few more benefits to this PR:
In conclusion, I strongly believe that this PR helps us fulfill our goal of making invalid states unrepresentable in the type system. Not merging it would not only hinder us from reaching that goal, but would undermine the work done by @ChrisThrasher with his previously merged PRs due to a glaring inconsistency between most SFML types and -- EDIT: I also wanted to point out that by merging this PR and #3015, @ChrisThrasher's concern about the pollEvents(window,
[&](sf::Event::Closed)
{
window.close();
},
[&](sf::Event::KeyPressed e)
{
if (e.code == sf::Keyboard::Key::Escape)
window.close();
else if (e.code == sf::Keyboard::Key::Space)
playAction();
},
[&](sf::Event::TouchBegan)
{
playAction();
}
[&](sf::Event::Resized)
{
sf::View view;
view.setSize({gameWidth, gameHeight});
view.setCenter({gameWidth / 2.f, gameHeight / 2.f});
window.setView(view);
}
); |
This is a good point I had not considered. I don't like the idea of having two functions with the same signature with different API promises about whether or not an event is returned.
Can you elaborate on how this PR relates to supporting visitation? |
I overstated my point, the same visitation API can be achieved even without this PR. The only difference is that without this PR, we'd have to inject a This PR removes the need for such a "hack", sort of proving that everything fits better together. Edited my original post. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose I can come around to this change. It is technically correct after all. My main argument about ergonomics is hard to support. Hopefully I'm wrong and users won't complain too much about using ->
so much in their event loops.
Still curious to hear from the rest of the team.
@@ -44,8 +44,8 @@ body: | |||
label: Steps to reproduce | |||
description: Tell us how to reproduce this issue and provide a [minimal, complete and verifiable example](https://stackoverflow.com/help/mcve), you can use the template below | |||
value: | | |||
1. | |||
2. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think these trailing newlines and the other trailing newlines in this file may be intentional
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trailing spaces are intentional, so you can start typing directly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will fix when I get back home
@@ -57,10 +57,13 @@ body: | |||
|
|||
while (window.isOpen()) | |||
{ | |||
while (const auto event = window.pollEvent()) | |||
while (const std::optional event = window.pollEvent()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a point of style I'd prefer we continue to use auto
here and in all other similar instances
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're basically doing that, only that we're constraining the type inference to a std::optional
of some T
. I think it's worthwhile using std::optional
here instead of auto
to clearly suggest that an event might or might not be polled.
I considered two alternatives:
auto
: hides too much information, not clear that astd::optional
is returned;std::optional<sf::Event>
: too verbose and redundant, the information about being an event is already clearly available from the name of the variableevent
.
Using std::optional
strikes a good balance between conciseness and providing useful information to the reader.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Before bringing up the same discussion point in every PR, maybe let's leave it as it is right now. Start a separate discussion and the align the code pieces afterwards.
It's not helpful when someone writes code in one way and someone else then goes and keeps changing it afterwards in their way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fair, but there was a reason I made this change specifically in this PR, as it was now unobvious that pollEvent
returns an optional with plain auto
.
window.close(); | ||
break; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose this break
ensures that event processing ceases when a close is requested? In theory this reduces the latency between clicking the close button the application actually closing. Is that your intent?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's not my intent, but it's a nice side benefit of the change.
The intention is to prevent event processing from causing issues when using waitEvent
, as waitEvent
now asserts m_impl != nullptr
, but WindowBase::close
immediately invokes m_impl.reset()
.
We need to call break;
to avoid accessing a deleted m_impl
on the next iteration.
Now that I wrote this, it does sound like a subtle trap if the break;
is forgotten... But the only way I can think of fixing this is to revert waitEvent
to return a std::optional<sf::Event>
, which loses a nice API improvement that this PR would provide.
I think a decent alternative would be to do this in waitEvent
:
Event WindowBase::waitEvent()
{
if (m_impl == nullptr)
{
sf::err() << "waitEvent invoked on destroyed window -- did you forget a `break`?" << std::endl;
std::terminate();
}
std::optional<sf::Event> event = m_impl->popEvent(true /* blocking */);
assert(event.has_value());
filterEvent(*event);
return *event;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added the additional safety measure to waitEvent
and updated its documentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, calling std::terminate()
is never an acceptable solution in my book.
Not sure if using an default constructed Closed event is really the solution. We've found an edge case where waitEvent
can return an empty state, as such I feel like it should be represented in the API. waitEvent
could return false in SFML 2, we shouldn't pretend that this state doesn't exist anymore in SFML 3.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've found an edge case where
waitEvent
can return an empty state
It's more of a precondition failure -- the precondition is that waitEvent
should not be called on a window that's already been closed. Returning a sf::Event::Closed
is a way of not terminating nor getting UB, but it's not behavior the user should rely on.
Perhaps I can add an err()
before the return to make it clear that the user is doing something wrong and we're just being nice?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, this just reminded me of the longstanding issue of
waitEvent
never returning if there no more events are being produced. For example if you runwaitEvent
in a separate thread and in the other thread closes the window. The event thread will continuing spinning as it doesn't get any events to process: https://github.com/orgs/SFML/projects/4?pane=issue&itemId=23482732# / #1184Since SFML is anyways doing a spinning while loop, due to Joystick event processing, I'd actually propose to change
waitEvent
to take a custom timeout parameter. Blocking methods should always have a way a cancellation token or timeout option.In that case you'll have another situation where waitEvent has a "failure case".
The user isn't doing anything wrong, when calling any of the window methods after closing the window. Because you still have a valid object and as such any operation on that object should remain valid for the lifetime of the object.
I like the timeout idea, I will work on a PR for that specifically (against master) and then integrate that into this one as they are separate concerns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created PR for the timeout here: #3094
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's more of a precondition failure -- the precondition is that waitEvent should not be called on a window that's already been closed.
In my opinion calling waitEvent
on a closed window is precondition violation. We should log an error, assert(false)
, then return sf::Event::Closed
to ensure behavior is defined in release builds. I still want that function to return an sf::Event
rather than std::optional<sf::Event>
.
I prefer we not add this break
everywhere. Let's only add it when waitEvent
is being used. Doing so will reduce the amount of code this PR touches and make me more eager to merge it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I strongly disagree. As long as an object was initialized, it's in a valid state. We cannot suddenly have functions semi-fail because we assign random preconditions.
Besides, with the timeout proposal, you actually get an expected failure case.
I'm also against adding break;
everywhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My views:
-
It's very unfortunate that we lose the nice non-
optional
API forwaitEvent
if we add the timeout, but I think it's a good idea to have one; -
I agree with @ChrisThrasher that calling
waitEvent
on a closed window should be a precondition violation and I agree with his proposed solution -- however if we do add a timeout it would make sense to returnstd::nullopt
there as well; -
I disagree with both of you regarding
break;
-- I think that the only sensible thing to do when you close a window is to break out of the event handling loop, and I'd like to promote that practice regardless of whether or not it's strictly necessary for the correctness of the program.
People can't simultaneously praise other hyped up languages for their at times really esoteric syntax and at the same time complain that C++ isn't staying up to date while complaining when ever so often we use already existing language constructs to make the language "less bad" than what its undeserved reputation has many laypeople believe. Sure I also prefer having to type less, but not at the cost of higher mental load because I have to think of something that wouldn't have been necessary if I would have typed a bit more. I prefer typing over thinking if there was a choice. I have used SFML for more than a decade at this point, and never have I spent more than 1% of total project time messing with event handling. If anyone spends so much time on event handling that this actually makes a difference they are doing something wrong anyway. |
695c250
to
3addaed
Compare
dab07c6
to
8411668
Compare
8411668
to
82e883b
Compare
82e883b
to
f66cef1
Compare
Proof of concept right now as I don't have access to a compiler, but I think this is a superior design compared to #2991.
Removing the empty state from event results in a much more type-safe API. Events cannot be default-constructed anymore, and APIs that poll events now return an optional event, making it clear that an empty state can be present without polluting usages of events that cannot be in such an empty state.
@ChrisThrasher: feel free to continue working on this branch to adjust the usages of event in tests and examples.