2017-06-01 14:15 GMT+02:00 Peter Dimov via Boost
Andrzej Krzemienski wrote:
// *operator->()
T& operator() & noexcept; T const& operator() const & noexcept; T&& operator() && noexcept; T const&& operator() const && noexcept;
This is operator*, right? This is technically narrow, but without >> essential benefits of direct narrow contract, as I tried to explain in >> another thread. I disagree.
What essential benefits are lost?
I tried to outline my reasoning in this post: http://boost.2283326.n4.nabble.com/outcome-narrow-contract- wide-contract-and-value-if-td4695003.html
In short, when the narrow contract is directly in the library's interface, I have a place where I can put BOOST_ASSERT(), or similar code for assisting instrumented builds.
That's what I was thinking - that you want to place an assert there - I just wanted to confirm that this is the only objection.
It is not _the only_ objection. I thought that this one would be the easiest to communicate across.
Do we agree that returning nullptr from operator-> is good for informing the static analyzer and the optimizer that a value is present?
result<X> r;
// ...
r->x; // same as __builtin_assume( has_value() )
X x = *r; // ditto
I cannot answer this question with certainty. It looks like it has the potential to address the static-analyzer/UB-sanitizer use cases. On the other hand, static analyzers are not ideal, and they get lost when the reason for UB spans across to wide space. It is conceivable that putting an ASSERT(), or some such, earlier (directly in `outcome` could help detect UB, but your trick with operator-> would not. But I have no data to support this.
I can understand the desire to assert() inside, but at the same time, view the idea of returning a X* to something that is not an X ....
Here, the first disagreement. The way I see it is that the blame is on the user side. I would put it, "requesting the call X* to something you know not to be an X is misguided".
as monumentally misguided in an error handling library.
A question to you. Is your verdict affected by the fact that this is "an error handling library" or that this is a "vocabulary type"? IOW, would your position be different if we were discussing a design of some bigger library, like ASIO. In yet other words, do you object the same to vector::operator[] allowing UB when over-indexed? I fail to be convinced that "error handling library" should make different decisions than any other library. It is my understanding that `outcome and `expected` are intended to carry the information about unavailability of resources, and similar conditions in the environment. In contrast, what we are arguing about is what happens upon programmer using the library incorrectly.
The goal here is to help people write robust code,
Here is the second disagreement. That is, I do agree with you (and I guess everybody agrees) that this is always the important goal: help people write robust code. I just fail to see how the decision to assign *just any* semantics to something that is a misguided decision (I mean "requesting the call X* to something you know not to be an X") helps people write robust code. It is not robust to requesting the call X* to something you know not to be an X and you are not preventing that.
not introduce subtle bugs
In general, you cannot protect form users planting bugs in their logic.
and security vulnerabilities by allowing them scribble all over the stack.
So, is this your goal? In case the programmer does not know what he is doing (that he is using the library incorrectly), the library itself should take actions to minimize the damage? I am somewhat convinced by that. Although I am not sure that this is the library that should take this responsibility. In my view, the library should provide information what the invalid usages are, and there should be another tool (like static analyzer) that does the job of minimizing damage.
(Note that with the above spec, you can still assert in op* and it would be conforming,
Do you mean that you are ok with operator* having a narrow contract?
and you can still have a mode in which you assert in op->, but it will not conform.)
But then I do not understand what you get in this compromise that operator* has narrow- and operator-> has a wide contract. And we should also bare in mind Niall's opinion, that if he agrees to a narrow contract, such function should spell longer than `value()`.
The root of our disagreement is the idea that undefined behavior is good because it supposedly allows you to do this or that.
It is not in this that I see the root of our disagreement. In fact, I do not yet see where this root is. What I fight for is not an UB but narrow contracts. The different between the two is the following. In order for UB to occur you require two things: a function, call it `f` with a narrow contract, and an another function that calls `f` out of the contract. In the ideal world you have functions with narrow contracts and nobody calling them out of contract. And defining contracts, as formally as possible, help users identify when they are violating the contract. Not every design requires narrow contract. For instance, if we allowed only variant-like inspection of `expected`, every usage makes sense and is wide in contract. But the design choice that you first ask in which of the states `expected` is in, and then "get_the_T` and `get_the_E` allows for incorrect usage. Narrow contract simply reflects the fact that there is a way to use this interface incorrectly. Narrow contract is not incorrect, it simply reflects the fact that there might exist incorrect programs.
It isn't, it never is.
No, I do not believe that UB guarantees something in run-time, or rely on it.
Defining the good things that we want to happen is good. Not defining them in the hope they'll occur is not.
I agree with this statement in isolation, but I think you misunderstood my position about narrow contracts. Regards, &rzej;