Open daviddetweiler opened 2 years ago
I'm also not the first person to have these concerns: https://github.com/isocpp/CppCoreGuidelines/issues/1561#issuecomment-616486172. There are four scenarios of "intended behaviors" I can think of:
Expects()
is a pure contract annotation (and is not required to do anything at runtime), and gsl::span
is a std::span
equivalent (bounds-safety is part of its contract, but is not required to be checked). The implication for Microsoft's GSL: pretty much none, except for a change to the guidelines to remove the current requirement that Expects()
/Ensures()
terminate on false.Expects()
is a runtime contract check and gsl::span
ensures at runtime that its bounds-safety is enforced. Changes: none, this is what happens right now, and the Microsoft implementation has already justified on basis of the current guidelines.Expects()
is a pure contract annotation and gsl::span
ensures bounds-safety at runtime; changes to the guidelines: span
probably shouldn't be the kosher suggestion the guidelines imply it is, as it is, by definition, pessimistic.Expects()
is a runtime contract check and gsl::span
does not do runtime checks; this possibility I don't think is even a remotely reasonable interpretation of the intent of the guidelines, as the "spirit" of span
is ultimately tied to that of Expects
.The question ultimately comes down to what is intended by the editors; as the guidelines are somewhat vague and possibly overly pessimistic.
I think we can agree that the general spirit of the guidelines is to ensure safety / correctness efficiently (hilosphy section claims as much; including avoiding "redundant checks").
Just saying: "efficiently" is not the same as "at zero (runtime) cost".
Editor's meeting: we looked at this in the past and backed off pending committee decision regarding Contracts, but we will take another look.
(Preface: the Microsoft implementation of the GSL is by far the most visible / vigorously maintained version, so when I refer to "the GSL", I refer to this implementation)
I think we can agree that the general spirit of the guidelines is to ensure safety / correctness efficiently (hilosphy section claims as much; including avoiding "redundant checks"). I've seen two places where the last concern might not have been applied so well. Part of this might very well be an issue specifically with the GSL implementation, but the guidelines as of the last few days seem to more-or-less encourage these issues.
Expects()
/Ensures()
being pessimized choices for contract statement. The rules encouragingExpects()
/Ensures()
are themselves fine: expose contracts directly to tooling, provide opportunity for runtime enforcement, etc. But the current specification of their behavior is that they terminate if false. Suddenly all contract checks (which these guidelines encourage to be used very frequently) "do something". Sure, the optimizer can probably see through the check in simple cases, but it just as often can't in the more complicated cases (MSVC in particular seems particularly conservative when reasoning about when a condition can/cannot be always true/false). It seems extremely pessimistic to ask for a generally difficult to optimize runtime check to be injected at every place an interface its used, considering how often the usage is statically correct. The GSL used to have support for essentially-free contracts (__assume(condition)
, etc.), removed them some time ago, and I have seen at least one stale PR in the GSL that aimed to reintroduce this configurability. This has ramifications forspan
andnot_null
as well, as both use these liberally.gsl::span
andgsl::not_null
. The latter's issue is simple: for plain pointers, assuming the correctness ofnot_null
itself, theExpects(/* inner wrapped pointer */ != nullptr)
found innot_null::get()
is unnecessary. The justification that the Microsoft implementation gives is a) this is a documentation of the contract b) this check isn't unnecessary for someunique_ptr
edge cases. Once again, asking for this check to be enabled for all cases where only one case actually needs it, whenExpects()
"does something", seems a pessimization. As forgsl::span
, I refer to the mandatory bounds-checking of its iterators. Take the logical equivalent of the ranged for-loop:While
clang
andgcc
are able to completely see through the entire loop, MSVC completely fails to eliminate at least one per-loop check at/O2
. The GSL implementers have taken to using the undocumented_Unchecked_begin
and_Unchecked_end
hooks to prevent otherwise grossly suboptimal code in thefor (auto& thing : span) {}
case. Now, ifgsl::span
is designed to pessimistically check all iterator operations and terminate on violations, I don't think it should be recommended as the kosher general-use alternative.std::span
made the right choice, in my opinion, by not requiring iterator validation, while leaving it open for such validation to be configured (similar to debug iterators in the MSVC STL). If it is instead, like thenot_null
case, meant to a documentation of its contract, we fall back to the issue with the pessimized behavior ofEnsure()
/Expect()
.(In fact, the LLVM version that ships as a drop-in replacement for MSVC in visual studio also seems to have issues in the simple case above)
I apologize if this is very long-winded issue; I have thought about this issue for some time now and have tried, as best as I can, to find the causes in both the guidelines and implementation decisions for the current state of affairs.