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.

> 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.

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.

> > >    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.
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 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.

Reply via email to