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)

Reply via email to