ned14 / status-code

Proposed SG14 status_code for the C++ standard

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Non-null mapping assertion in quick status code implementations

cstratopoulos opened this issue · comments

I've noticed that a bunch of the quick_status_code_from_enum_code methods have something like the following:

 virtual generic_code _generic_code(const status_code<void> &code) const noexcept override
  {
    assert(code.domain() == *this);  // NOLINT
    const auto *mapping = _find_mapping(static_cast<const quick_status_code_from_enum_code<value_type> &>(code).value());

    //// THIS LINE
    assert(mapping != nullptr);
    //////

    if(mapping != nullptr)
    {
      if(mapping->code_mappings.size() > 0)
      {
        return *mapping->code_mappings.begin();
      }
    }
    return errc::unknown;
  }

This has been causing some problems in testing/debugging for me--I get that in any implementation methods for status codes the assert(code.domain() == *this) is required because it would be a genuine error to call them with another domain. But verifying that the mapping is non-null strikes me as more of a Wextra or Wpedantic type thing--in the example above if assert expands to nothing then we just get an errc::unkown, and then in status_code<void>::equivalent, it looks like it will refuse to do the generic code comparison if no mapping is provided, which strikes me as a sensible approach:

template <class T> inline SYSTEM_ERROR2_CONSTEXPR14 bool status_code<void>::equivalent(const status_code<T> &o) const noexcept
{
// ...
    generic_code c1 = o._domain->_generic_code(o);
    if(c1.value() != errc::unknown && _domain->_do_equivalent(*this, c1))
    {
      return true;
    }
    generic_code c2 = _domain->_generic_code(*this);
    if(c2.value() != errc::unknown && o._domain->_do_equivalent(o, c2))
    {
      return true;
    }
  }
// ...
}

Just as motivation/digression, suppose some external ThirdPartyApp has online docs with a mountain of hundreds of error codes, but I define enum class ThirdPartyCodes for the 5-10 that my application can meaningfully respond to. If I'm grabbing an error code value from an HTTP response or some API call, I might do static_cast<ThirdPartyCodes>(int_val) to get a status_code out of it (via quick_status_code_from_enum). So in some but not all cases I would have a mapping entry for it, but just want to fall back to errc::unknown in any of the other cases.

I do think it is a meaningful use-case to require that the mapping is exhaustive, in which case a developer would be equally as alarmed to find out that the mapping returned null as they would be to find out they were casting codes to the wrong domain. But in a scenario like the one above it seems like no big deal if some or many codes just get punted to errc::unkown.

I'm not sure of the best route forward, some of the first ones that come to mind are a policy template parameter for whether the mapping must be exhaustive, a virtual method to get called on null mapping, or maybe providing the null check (or not) through a traits class or mixin. Alternatively it could be a second-class assertion macro which can be disabled with a compile definition.

I can see why this is could be useful. However, I'm almost certain that this can't just be added to the existing standardization proposal, but requires an update paper.

I'm not sure of the best route forward

A static constexpr bool or a bool_constant alias in quick_status_code_from_enum with a default in quick_status_code_from_enum_defaults would be my preferred approach.

Alternatively it could be a second-class assertion macro which can be disabled with a compile definition.

I'm weakly against this approach as it simply does not compose.


Please also remember that you can still define your own domain, even if it is much less concise.

Yeah I was wondering about the paper as well--I just did a skim and it seems like the quick status code stuff has pretty minimal exposition. I guess my main questions would be whether
a) for other domain types, writing your own domain, etc. there is wording for assert(code.domain() == *this) as runtime UB or IFNDR or whatever (I feel like this is required explicitly?) and
b) if there should be wording that either stipulates that the quick_status_code_xxx stuff should have an exhaustive mapping or not, this seems maybe more important from a standardization/wording perspective and a precondition to considering the escape hatch routes

I agree your approach is probably the best and with your misgivings about the debug assert approach, was just kind of thinking that it's a weird situation where DNDEBUG will remove the assert and fall back to a (perhaps for many purposes) totally acceptable behavior

edit: Oh and yes for my purposes I can totally define my own domain or just override some of the virtual stuff from quick status code

for other domain types, writing your own domain, etc. there is wording for assert(code.domain() == *this) as runtime UB or IFNDR or whatever

My guess would be runtime UB, but I don't think that any standardese regarding this has been written. The API design actually prevents this from happening for most (all?) idiomatic usages. The only case I can think of is a (custom) status_code_domain implementation deliberatly calling one of its methods with a mismatching status_code. So as far as I can see, the assert is more of a debug helper for the status-code code base.

if there should be wording that either stipulates that the quick_status_code_xxx stuff should have an exhaustive mapping or not.

That's a question Niall needs to answer.

Oh and yes for my purposes I can totally define my own domain or just override some of the virtual stuff from quick status code.

If you can use C++20 and don't have multiple equivalent system_error2::errc values per code, you could also use my data_defined_status_domain.

I've been thinking about this since yesterday.

It seems to me that asserting on not finding a mapping in equivalence testing and getting a generic near equivalent is excessive, so my proposal is that those asserts be removed entirely.

Thoughts? Would this solve the OP's issue?

I agree it's probably excessive. From the point of view of my use case (which I don't want to harp on too much, because I also could simply define a custom domain) I think the asserts would still get hit in _do_failure and _do_message as well.

I think in _do_failure it could also be removed. Basically we're just saying if you can map it to errc::success it's not a failure, else it is--if there's no mapping then we have certainly failed to map it to errc::success.

In _do_message this also has a fallback which simply prints "unknown", which is also fine for my use case. From a wording perspective maybe this is the same as saying it'll print the same thing as generic_code(errc::unknown).message(), or there's the classic "implementation-defined NTBS" like for std::exception::what.

If that's not adequate, maybe users could also provide a static constexpr auto unknown_code() method that says, like, if you can't map a given code, just punt it to ThirdPartyCodes::unknown_code and stringify accordingly.

A specification like "no mapping" implies "mapped to errc::unknown" sidesteps the need for a case-by-case specification and is also easy to remember/teachable.

It might be useful to additionally provide a consteval function that tests a given quick_status_code_from_enum for correctness, i.e. no duplicated entries and/or an entry for every value up to a LIMIT for dense error codings...

I finally got a LLFIO release out the door last week after endless CI fun, so this is my next item to address in my very scarce free time.

Your issue actually came up at work yesterday when I used quick_status_code_from_enum. The assert fired because I had forgotten to add a mapping for an enum value into the mapping table, which was obviously my original motivation for adding the assert.

I'm thinking of adding an explanatory comment for most of the asserts, but remove them where they would be excessively strict.

Hopefully this addresses your issue. Let me know if it doesn't. Thanks for reporting it!