On Tue, 3 Aug 2021, 23:10 Modi Mo, wrote: > > On 8/3/21, 1:51 PM, "Jonathan Wakely" <jwakely....@gmail.com> wrote: > > > > Could you explain this sentence in the commit message: > > "Note that the definition of global new is user-replaceable so users > > should ensure that the one used follows these semantics." > > AFAICT based on the C++ standard, the user can replace the definition of > operator new with anything they want.
No, they have to meet certain requirements. If any operator new is potentially throwing (i.e. not declared noexcept) and it returns null, you get UB. A potentially throwing operator new *must* return non-null, or throw something they can be caught by catch(bad_alloc const&), or not return (e.g. terminate). If any operator new is noexcept, then it reports failure by returning null instead of throwing. So if the user replaces the global operator new(size_t), then their replacement must never return null, because it's declared without noexcept. > > In the current implementation we're using an un-enforced "throw()" annotation > so it's up to the implementation of ::new (in libstdc++/jemalloc etc.) to > comply properly or risk UB. That's what I was afraid of. The implementation in libstdc++ cannot know the user passed -fnew-infallible so has no way of knowing whether it's allowed to throw, or if it has to terminate on failure. So it will throw in failure, which will then be undefined. So it seems the user must either provide a replacement operator new which terminates, or simply ensure that their program never exceeds the system's available memory. In the latter case, the -fnew-infallible option is basically a promise to the compiler that the system has "infinite" memory (or enough for the program's needs, if not actually infinite). > Marking ::new as noexcept is more rigid of an enforcement but I believe it's > implementation defined as to how exception specifications are merged I think it's undefined if two declarations of the same function have different exception specifications. I might be wrong. >and because noexcept is not part of the mangled name link-time replacement >would encounter the same issue. Yes. Making it noexcept would currently mean that the compiler must assume it can return null, and so adds extra checks to a new-expression so that if operator new returns null no object is constructed. But if it's noexcept *and* return_nonnull I guess that check can be elided. > > > What happens if operator new does throw? Is that just treated like any > > other noexcept function that throws, i.e. std::terminate()? Or is it > > undefined behaviour? > > It gets treated like the older "throw()" annotation where it's UB, there's no > enforcement. It's not UB for a throw() function to throw. In C++03 it would call std::unexpected() which by default calls std::terminate(). So throw() is effectively the same as noexcept. > > > And what if you use ::new(std::nothrow) and that fails, returning > > null. Is that undefined? > > This change doesn't affect ::new(std::nothrow) ATM. Nathan Sidwell suggested > that we may want to change semantics for non-throwing as well to be truly > "infallible" but we don't have a use-case for this scenario as of yet. Ah, I see. So it only affects operator new(size_t)? And operator new(size_t, align_val_t) too? And the corresponding operator new[] forms? > > > It seems to me that the former case (a potentially-throwing allocation > > function) could be turned into a std::terminate call fairly easily > > without losing the benefits of this option (you still know that no > > call to operator new will propagate exceptions). > > Could you elaborate? That would be great if there's a way to do this without > requiring enforcement on the allocator definition. Well I meant adding enforcement when calling operator new. The compiler could treat every call to operator new as though it's done by: void* __call_new(size_t n) noexcept ( return operator new(n); } so that an exception will call std::terminate. But that would add some overhead (more for Clang than for GCC, due to how noexcept is enforced), and that overhead is presumably unnecessary if the intention of the option is to make a promise that operator new won't ever throw. > > > But for the latter case the program *must* replace operator new to > > ensure that the > > nothrow form never returns null, otherwise you violate the promise > > that the returns_nonnull attribute makes, and have undefined > > behaivour. Is that right? > > Yes good insight. That's another tricky aspect to implementing this for > non-throwing new. If you just treat -fnew-infallible as a promise that operator new never fails, then ISTM that it's OK to treat failure as undefined (you said failure was impossible, if it fails, that's your fault). And if you replace operator new, you can keep your promise by making it terminate on failure. But if some enforcement is wanted (maybe via -fnew-infallible-trust-but-verify ;-) then calls to non-throwing new could be enforced as if called by: void* __call_new_nt(std::size_t n) noexcept { if (auto p = operator new(n, std::nothrow)) return p; std::terminate(); } Anyway, all that aside, I find the idea interesting even if it's just an unchecked promise by the user that new won't fail. I wonder how far we could get just by changing the <new> header, so that a -D option could be used instead of -fnew-infallible: --- a/libstdc++-v3/libsupc++/new +++ b/libstdc++-v3/libsupc++/new @@ -123,8 +123,14 @@ namespace std * Placement new and delete signatures (take a memory address argument, * does nothing) may not be replaced by a user's program. */ +#ifdef _GLIBCXX_NEW_INFALLIBLE +_GLIBCXX_NODISCARD __attribute__((return_nonnull)) + void* operator new(std::size_t) noexcept + __attribute__((__externally_visible__)); +#else _GLIBCXX_NODISCARD void* operator new(std::size_t) _GLIBCXX_THROW (std::bad_alloc) __attribute__((__externally_visible__)); +#endif _GLIBCXX_NODISCARD void* operator new[](std::size_t) _GLIBCXX_THROW (std::bad_alloc) __attribute__((__externally_visible__)); void operator delete(void*) _GLIBCXX_USE_NOEXCEPT (and similarly for the new[] and aligned new overloads)