During a quick skim through the docs: in "Expected
in Practice" section "The tension between type safety and convenient programming", it's unclear why the final example is any "better" than the ones preceding it.
I had thought the text pretty clear that it isn't better per se, but rather a fusion of the approaches where the type of E is used to prevent code from returning an invalid-for-that-domain error. Some would (and indeed have) call it a worst of both worlds, but it does balance a reasonable amount of getting the compiler to enforce incommensurate error code domains at compile time, with all error codes being subclasses of std::error_code, and thus one can strip the type safety in code further up the call stack such that all error domains feed into a common error_code based error handling system.
In particular, if you're going to explicitly add a constructor to MathError2 such that it knows how to construct itself from MathError1 (as in the final example), why would you not put the switch statement from the first example into that constructor, such that it actually translates the error codes into the expected domain? Especially when there is no loss or overlap in doing so, as in these examples?
If you're writing a switch statement or doing any form of mapping with
error codes, you're doing it wrong from C++ 11 onwards.
One always wants to preserve and propagate exactly the original error
code, unmodified in any way. That's the whole point of
Granted std::error_code does let you transport "foreign" error codes, but surely translating recognised codes from a method's internal implementation to those of its external interface is desirable? (Though perhaps that may depend somewhat on your views of encapsulation and implementation hiding.)