On Wed, 4 Aug 2021, 02:15 Modi Mo, <mod...@fb.com> wrote: > On 8/3/21, 4:57 PM, "Jonathan Wakely" <jwakely....@gmail.com> wrote: > > > 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. > > Ah right, I found this out when implementing which is the main motivation > behind using "throw()" as noexcept will categorize all ::new as > ::new(std::nothrow) which isn't what we're after in this implementation. >
This seems like a clang-specific implementation detail. I think GCC treats throw() as exactly equivalent to noexcept. But maybe __attribute__((nothrow,return_nonnull)) would do it for GCC. > > 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). > > Yeah, there's a disconnect between declaration (what -fnew-infallible > does) and definition (libstdc++). I don't know of a way to enforce this > automatically without creating a shim like you suggested. > > > > 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. > > The main reason I didn't do this is as you identified above, marking it > noexcept from a standard viewpoint changes ::new to : :new(std::nothrow). > > > 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. > > I think doing this could work though I feel like there'll be some rough > edges that have to be filed down. > > > > > 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. > > What's getting altered ATM is the implicit declaration of "new" that > doesn't actually require the header to be copied in: > > These implicit declarations introduce only the function names operator > new, operator new[], opera- tor delete, and operator delete[]. [Note: The > implicit declarations do not introduce the names std, std::size_t, > std::align_val_t, or any other names that the library uses to declare these > names. > Thus, a new-expression, delete-expression, or function call that refers to > one of these functions without importing or including the header <new> > (17.6.1) is well-formed. > Ah right, yes, it's not enough to adjust <new>, so it needs to be a compiler flag. > The definition though doesn't have to abide by this and if it doesn't then > UB gets triggered. > > > > > 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? > > Yep. > > > > > 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. > > The extra overhead is pretty meaningful as when inlined you'll get > terminate landing pads at each site and when not inlined you get an extra > call. > These landing pads are not present in GCC, as the EH tables are used instead. > > > > 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. > > Agreed, from a design standpoint operator new failing with > -fnew-infallible is just UB. > > > 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. > > :D > > > 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) > > I tried this initially but hit exception spec conflicts against the > implicit definitions that the Clang FE generates which are unchanged. > The implicit declarations are also relevant in more cases where <new> > isn't included. > Understood. On the subject of adding a libstdc++ -D option in this area: > > libstdc++-v3/include/ext/new_allocator.h > > // NB: __n is permitted to be 0. The C++ standard says nothing > // about what the return value is when __n == 0. > _GLIBCXX_NODISCARD _Tp* > allocate(size_type __n, const void* = static_cast<const void*>(0)) > { > #if __cplusplus >= 201103L > // _GLIBCXX_RESOLVE_LIB_DEFECTS > // 3308. std::allocator<void>().allocate(n) > static_assert(sizeof(_Tp) != 0, "cannot allocate incomplete > types"); > #endif > > if (__builtin_expect(__n > this->_M_max_size(), false)) > { > // _GLIBCXX_RESOLVE_LIB_DEFECTS > // 3190. allocator::allocate sometimes returns too little > storage > if (__n > (std::size_t(-1) / sizeof(_Tp))) > std::__throw_bad_array_new_length(); > std::__throw_bad_alloc(); > } > > With the goal of reducing exception propagation, the exception here from > exceeding _M_max_size() which is 2^64 bits on 64-bit systems should It should not be more than 2^63 on typical systems: #if __PTRDIFF_MAX__ < __SIZE_MAX__ return std::size_t(__PTRDIFF_MAX__) / sizeof(_Tp); #else return std::size_t(-1) / sizeof(_Tp); #endif never happen but does pessimize propagation if it can't be inlined/proven > untaken. We're looking at a -D option "_GLIBCXX_RESTRICT_EXCEPT" (name TBD) > to mark functions like these as noexcept. With ::new being infallible a lot > of these functions (like _S_check_init_len in stl_vector.h as well) have > these very unlikely corner cases that now become the exceptional roots and > could be optimized optimistically now. > I'm interested in this too, let me know when you want to pursue this (probably on the libstdc++ mailing list).