-
Notifications
You must be signed in to change notification settings - Fork 256
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
Proposed update to remove RAII problem #251
Conversation
I propose the removal of the use of `std::exit`. It's usage breaks RAII which is not mentioned in the documentation. As for the addition of the exception, during parsing we already require that the user catches several exceptions thus the whole `try/catch` harness is already there for a proper program. Adding a catch clause for the new `NormalProgramTermination` is a bit bulky but doesn't break RAII and is explicit on that the program should terminate in normal usage.
I disagree with this change. This requires every program without overridden "--help" and "--version" to explicitly handle an exception for non-exceptional usage. The current behavior is to let basic users focus on their arguments and allow advanced users to override the default handlers for advanced needs. Yes, |
RAII is not a not special clean-up procedure and not executing dtors is a critical breakage with core functionality of C++. It at the very least should be explicitly clear that dtors might not be called in some execution paths of the library. Maybe the snippet below would be possible as a usage pattern. This allows termination without exceptions but doesn't do it by calling
Sure it requires setup by the user, that is however the tools that the languages gives us to handle these scenario's and to reiterate, |
FWIW I agree with this change; I ran into this problem in my own program and thought it was a very goofy part of the design - not honouring idiomatic RAII by default is just confusing C++. |
Hi. Thanks for the PR and the comments. Your points regarding RAII are valid. One of the reasons why this library chooses to exit after printing help is because Python's argparse exits after printing help. As you can imagine, much of the API is designed based on Python's argparse. We call exit; we don't call abort. Yes, it is bad that destructors are not called when this happens, I don't deny this. We already have a constructor argument to include or exclude default arguments (help, version). How about adding a second argument that configures whether or not the library will exit the program? This can be documented up front. Python's argparse has a similar exit_on_error constructor argument (default: true). explicit ArgumentParser(std::string program_name = {},
std::string version = "1.0",
default_arguments add_args = default_arguments::all,
exit_on_default_arguments = true) If we allow the user to configure whether or not exit will happen, the user can then handle this issue however they see fit. As @skrobinson mentioned, the user can already override the behavior today by providing their own version of these default arguments which does not exit the program but instead throws a custom exception. There doesn't seem to be a clear reason to add this custom exception type to the library. I'd be happy to document that the default behavior of the program is to exit and that there are ways to prevent that from happening. |
I'd argue that "do the correct C++ thing by default" is reason enough, personally. Copying python's argparse breaks down once you run afoul of the fact that C++ is not, in fact, Python, and thus has different semantics. If the current behaviour is to be preserved, there should at least be a warning in big red letters in the README and elsewhere. |
I agree with @marzer, we are coming up against a difference between what C++ and Python can do. Proper handling in C++ means doing it in a different way than Python. It is of-course always preferred if a codepath can be handled internally but C++ doesn't allow for that in this situation. Users of C++ are accustomed to having to handle details sometimes and providing a easy, clear, concise and proper way of doing it should be the priority. Doing it wrong by default isn't right. It also teaches others to disregard the core components of C++ for a reduction in outer handling code. Adding a library internal |
An argument in favor of using the error handling scheme of exceptions is that they are made to keep our programs in a determinant state by allowing for cleanup when program termination is needed. Normally we need proper program termination because of an egregious program error internally. In this situation it is 'normal' behaviour we want and everything went correctly, however we still require by default that a program terminates when called with the |
What I meant is that for many cases, exiting the process and returning the program's resources to the host environment is sufficient. Funnily enough, I don't disagree with most of your argument. But, I see our current behavior as a way to make simple cases simple in a way that many potential users will not find surprising. Using the name Since PR #142, we have allowed the default arguments to be disabled by those users that have different needs or preferences. |
If you're going to stick with this as default, the RAII implications need to be documented accordingly. Simply making the word would be the minimum expectation for something so non-idiomatic, imo. |
Also, as software becomes increasingly cross-platform, this assertion is misguided, and I submit that it's actually the majority that will need proper handling, even without realising. Many applications need to initialise the terminal (for coloured output, virtual terminal on windows, Unicode handling modes, etc), and generally that needs to happen before argparse is even instantiated. A good program will then reverse these changes on exit so that the user's terminal is left in the same state it was at the beginning. C++ RAII is a perfect way to handle this robustly. |
I understand this drive and I fully agree with it. Having used both, I can see how well it was achieved. We however need to recon with the fact that python and C++ are fundamentally different in some aspects. Another possibility is to introduce non portable code, e.g. calling
Not to mention that this is half way down the instructions buried under an obscure header of "default arguments" and doesn't mention anything about the implications for RAII. |
You've both argued so strongly for this that I re-examined my view on the general matter of
Trying to improve RAII support by throwing on To be explicitly clear, I don't believe you're wrong. Instead, this is a design decision that attempts to start simple while allowing more rigorous use where needed. |
For some definition of normal. It skips a normal part of C++, so I'd say very definitively no it is not. It's normal from the OS's perspective, and that's all. Meaningless from the perspective of the points being raised here.
That is unfortunate, because this opinion will open this library up to being the source of one or more CVEs. It probably already has, indirectly. If you/the library owner insist on maintaining this stance, then I'd like to reiterate that the existing documentation is woefully insufficient on this point. It should be very clearly demarcated with separate warning paragraph in the README, e.g.:
I'd also recommend you replace all appearances of (I am also happy to make these documentation changes myself via a PR, of course.) |
Why do you recommend using To repeat:
You make a good point here. I'd support a PR with that change. The |
I'm not necessarily recommending one over the other, I'm saying the README should appropriately cover both scenarios, including caveats. Currently it does not do so sufficiently. Customizing
I understand that. Comprehension is not the issue. The problem is that it's a bad default. The default behaviour should be the safe behaviour. A some-time-in-the-future major version bump of the library should change this, IMO. Currently it's trying just a bit too hard to copy Python. There's a backwards-compatible middle-ground where you get the best of both worlds that can be implemented without using an exception (as that seems to be a sticking point here): The parser object could store some state allowing programmer to query if the user invoked one of the defaults, combined with a flag that tells the parser not to exit automatically (@p-ranav's argparse::ArgumentParser program(
"compiler",
"1.0",
default_arguments::all | default_arguments::do_not_exit
);
program.parse_args(argc, argv);
if (program.invoked_default_argument())
return 0; This is vastly superior IMO, because you still get all the convenience of the default
FYI if I make a PR with that change, my PR would also include the RAII warning. I don't think it would be all that useful without it. |
I've made a first-draft implementation of @p-ranav's |
Looks good so far, but you need to also pair it with some way of querying whether a built-in default argument was invoked (e.g. my EDIT: actually, does |
Yes, program.parse_args(argc, argv);
if (program.is_used("--help") or program.is_used("--version"))
return 0; |
Well then, consider me on board, heh. That's a good alternative for people not wanting a forced |
@trick2011 Does PR #264 meet your needs? |
@skrobinson #264 is a fine step forwards to a better version and I definitely approve of its inclusion in the main codebase. I still disagree with the default behaviour being a call to |
I propose the removal of the use of
std::exit
. It's usage breaks RAII which is not mentioned in the documentation.As for the addition of the exception, during parsing we already require that the user catches several exceptions thus the whole
try/catch
harness is already there for a proper program. Adding a catch clause for the newNormalProgramTermination
is a bit bulky but doesn't break RAII and is explicit on that the program should terminate in normal usage.