C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Std-Proposals Digest, Vol 52, Issue 57

From: trtaab trtaab <tvfvof_at_[hidden]>
Date: Sun, 16 Jul 2023 02:39:57 +0000
When will be herbceptions??

Sent from Mail<https://go.microsoft.com/fwlink/?LinkId=550986> for Windows

From: std-proposals-request_at_[hidden]<mailto:std-proposals-request_at_[hidden]>
Sent: Saturday, July 15, 2023 20:41
To: std-proposals_at_[hidden]<mailto:std-proposals_at_[hidden]>
Subject: Std-Proposals Digest, Vol 52, Issue 57

Send Std-Proposals mailing list submissions to
        std-proposals_at_[hidden]

To subscribe or unsubscribe via the World Wide Web, visit
        https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
or, via email, send a message with subject or body 'help' to
        std-proposals-request_at_[hidden]

You can reach the person managing the list at
        std-proposals-owner_at_[hidden]

When replying, please edit your Subject line so it is more specific
than "Re: Contents of Std-Proposals digest..."


Today's Topics:

   1. Re: Forced stopping of RAII compliant execution threads.
      POSIX threads. (Jason McKesson)
   2. Return Value Optimisation whenever you need it (guaranteed
      elision) (Frederick Virchanza Gotham)
   3. Re: Return Value Optimisation whenever you need it
      (guaranteed elision) (Jason McKesson)
   4. Re: Impact of defaulted ctor on value initialization
      (Magnus Fromreide)


----------------------------------------------------------------------

Message: 1
Date: Sat, 15 Jul 2023 12:35:14 -0400
From: Jason McKesson <jmckesson_at_[hidden]>
To: std-proposals_at_[hidden]
Subject: Re: [std-proposals] Forced stopping of RAII compliant
        execution threads. POSIX threads.
Message-ID:
        <CANLtd3VyEHqk2Y3scmzg9kR9QLA8QhHNgXBs=8O5-OS9NqJ5qQ_at_[hidden]>
Content-Type: text/plain; charset="UTF-8"

On Sat, Jul 15, 2023 at 12:08?PM Yuri Petrenko via Std-Proposals
<std-proposals_at_[hidden]> wrote:
>
> Yes, "throwing an exception into someone else's call-stack" is a good model. "throwing an exception into someone else's call-stack with trigger condition" is a more accurate model. I wrote recently "there are certain intervals at which we can/can't destroy it". Of course, a programmer should be able to mark such unsafe intervals with a flag.

So code is assumed to be safe unless it is marked unsafe? That's...
*horrifyingly* dangerous. Especially for a circumstance that is a
fairly rare occurrence.

One of the reasons why maintaining exception-safety is so difficult is
that C++ makes it really easy to *think* your code is exception safe
when it really isn't. And you're trying to open up a second hole like
that.

And it should be noted that this is even harder than maintaining
exception-safety. Why? Because *any function* can be cancelled. Even
one you didn't write yourself.

If you call a C library function, that function is incapable of
throwing an exception. But it would be capable of being "canceled". So
your code might have been exception safe, but it isn't cancel safe.

Having to proactively, defensively code *everything* around the
possibility of thread cancellation at any time (on top of all of the
other things C++ programmers have to consider) is just not a good
idea. This would make much more sense in a language like Rust, which
had safe and unsafe blocks of code from day one.

> While we are in an unsafe section, we execute the program code normally, as soon as we leave the unsafe section we throw an exception. The second condition that checks our "exception with a trigger condition" is the thread stopping policy. If an immediate, unsafe stop is requested from outside, it will happen like that, some resources will leak (if we got into a dangerous place from the point of view of leaks), but not all of them and this is still better than TerminateThread.
>
> ??, 15 ???. 2023??. ? 16:40, Jason McKesson via Std-Proposals <std-proposals_at_[hidden]>:
>>
>> On Sat, Jul 15, 2023 at 9:27?AM Yuri Petrenko via Std-Proposals
>> <std-proposals_at_[hidden]> wrote:
>> >
>> > It's great that Linux already has something to rely on, but there is a subtle difference. In the case you mentioned, as far as I understand, pthread_cancel will ask the thread to cancel, and this will be possible only when we reach the Cancelation point, this mechanism is much better than a stop token, but very similar to it. In case of RAII thread we can interrupt the execution thread immediately without waiting for Cancelation point or token check, except for a few intervals like prologue and epilogue, destructor (destructors need to be discussed in detail), etc., where special handling is needed.
>> > In case of pthread there are points at which we cancel the thread, in case of RAII thread there are certain intervals at which we can/can't destroy it. The gist of this idea is that if the thread corresponds to RAII as a whole, it is not necessary to wait for Cancellation point.
>>
>> Is it though? Is it really?
>>
>> Even code that follows RAII principles religiously will have places
>> where cancellation at various points will cause breakage. I'm going to
>> model your cancellation feature as "throwing an exception into someone
>> else's call-stack".
>>
>> Consider `make_unique`. Somewhere in this function is code that
>> reduces down to this:
>>
>> unique_ptr<T>(new T(...));
>>
>> In the evaluation of that expression, there is a point where `new T()`
>> has been executed but *not* the constructor of `unique_ptr`. Until
>> that constructor has finished executing, the pointer is not owned by
>> anyone.
>>
>> However, this is fine. `unique_ptr`;s constructor is `noexcept`, the
>> initialization of its parameters are all `noexcept`, and if the `new`
>> expression throws, there is no pointer to manage. So between a
>> successful evaluation of `new T` and the completion of `unique_ptr`'s
>> constructor, no exceptions can possibly be thrown. Therefore, this
>> code is exception-safe.
>>
>> If you can cancel a thread (throw an exception into the call-stack) at
>> any point, then you could cancel it at the precise point where no
>> exceptions could have otherwise been thrown. That's... bad.
>>
>> Code can be exception-safe, but that's only because there are places
>> where it is guaranteed that no exceptions can be thrown. Cancellation
>> *must* work the same way: there must be places where you *cannot*
>> cancel a thread. RAII can encapsulate those into constructors and
>> functions returning prvalues, but that just puts the uncancellable
>> code inside of specific blocks. It still exists and still must be
>> uncancellable.
>>
>> > In general, I would really like to use the handler mechanism you mentioned. I'll be sure to add the relevant part to the discussion, thank you.
>> > --
>> > Std-Proposals mailing list
>> > Std-Proposals_at_[hidden]
>> > https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>> --
>> Std-Proposals mailing list
>> Std-Proposals_at_[hidden]
>> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
>
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals


------------------------------

Message: 2
Date: Sun, 16 Jul 2023 00:58:03 +0100
From: Frederick Virchanza Gotham <cauldwell.thomas_at_[hidden]>
To: std-proposals <std-proposals_at_[hidden]>
Subject: [std-proposals] Return Value Optimisation whenever you need
        it (guaranteed elision)
Message-ID:
        <CALtZhhNs8cwHLeYpUoa34tk5EoB+CPqngRLfFfMGDNi=m0595w_at_[hidden]>
Content-Type: text/plain; charset="UTF-8"

We can return a mutex by value from a function as follows:

    std::mutex Func(void)
    {
        return std::mutex();
    }

We can do this even though std::mutex can't be moved nor copied. This
is called 'Return Value Optimisation', and it's mandatory for the
compiler to elide the move/copy operation.

But let's change it a little:

    std::mutex Func(void)
    {
        std::mutex mtx;
        return mtx;
    }

Now it no longer compiles. What we have here is 'Named Return Value
Optimisation'. In this circumstance, the compiler may elide the
move/copy operation if it wants to, but still the move/copy
constructor must be accessible -- therefore it won't work with an
std::mutex.

So this begs the question.... Is it at all possible to return a locked
mutex by value from a function? Is it possible to somehow do the
following?

    std::mutex Func(void)
    {
        std::mutex mtx;
        mtx.lock();
        return mtx;
    }

In the System V x86_64 calling convention (which is used by every
x86_64 operating system apart from Microsoft Windows), if a function
returns a class by value, the object is constructed at the address
pointed to by the RDI register. So in theory, if a function wanted to
return a locked mutex by value, all it would need to do is:
(Step 1) Construct a mutex at the address pointed to by RDI
(Step 2) Lock the mutex at the address pointed to by RDI
(Step 3) Return from the function

I'm going to make a little change to 'Func' which I wrote above. I'm
going to change it to the following:

    std::mutex Func( void(*const f)(std::mutex*) )
    {
        std::mutex mtx;
        f(&mtx);
        return mtx;
    }

And so now you can provide your own function to manipulate the object
however you wish. Let's keep it simple and write a lambda to lock the
mutex, and so then we would invoke 'Func' as follows:

    int main(void)
    {
        Func( [](std::mutex *const p) { p->lock; } );
    }

Before I write the x86_64 assembler for 'Func', I'm going to write a
little helper function that will construct the mutex for me:

    void construct_mutex(void *const arg)
    {
        ::new(arg) std::mutex();
    }

(NB: The implementation of the constructor for std::mutex isn't to be
found in the libstdc++ dynamic shared library as it always gets
expanded inline, and so that's why I need the above helper function
instead of just directly invoking the constructor from assembler).

And so now I'll write the x86_64 assembler for 'Func'. If you look at
the function signature for 'Func', we know what will be stored where:
    RDI : Address of return value
    RSI : Address of lambda

Note that RDI and RSI are caller-saved registers, and so I'll push
them before every function call and then pop them again afterward.
Here's the GNU inline assembler for 'Func':

__asm("Func: \n"
      ".intel_syntax noprefix \n"
      " push rdi \n" // save to restore later
      " push rsi \n" // save to restore later
      " call construct_mutex \n" // construct mutex at
return value
      " pop rsi \n" // restore rsi after call
      " pop rdi \n" // restore rsi after call
      " call rsi \n" // call manipulator function
      " ret \n"
      ".att_syntax");

It's really that simple. We can now make use of 'Return Value
Optimisation' wherever we want it, and we can indeed return a locked
mutex by value. Check it out:

    https://godbolt.org/z/WWzWs6zEY

This is really easy to pull off on Linux and Mac. I haven't looked at
Microsoft, nor have I looked at aarch64 nor arm32. I wonder if it's as
easy on those architectures too.

But anyway, maybe we could make the elision of the copy/move operation
mandatory for NRVO by making a language change to allow the following
syntax:

    std::mutex Func(void) -> NRVO(mtx)
    {
        mtx.lock();
        return mtx;
    }

The compiler would treat the above function as though it had been written:

    std::mutex Func(void)
    {
        std::mutex mtx;
        mtx.lock();
        return mtx; // but with the guarantee of elision
    }

I don't think it's reasonable that C++23 still can't return a locked
mutex by value. We should do something about this for C++26.


------------------------------

Message: 3
Date: Sat, 15 Jul 2023 20:26:14 -0400
From: Jason McKesson <jmckesson_at_[hidden]>
To: std-proposals_at_[hidden]
Subject: Re: [std-proposals] Return Value Optimisation whenever you
        need it (guaranteed elision)
Message-ID:
        <CANLtd3XhUbBL4WdLnUCBVapdgdnseZzwE6sqkBHAt1fGyYK+zA_at_[hidden]>
Content-Type: text/plain; charset="UTF-8"

On Sat, Jul 15, 2023 at 7:58?PM Frederick Virchanza Gotham via
Std-Proposals <std-proposals_at_[hidden]> wrote:
>
> We can return a mutex by value from a function as follows:
>
> std::mutex Func(void)
> {
> return std::mutex();
> }
>
> We can do this even though std::mutex can't be moved nor copied. This
> is called 'Return Value Optimisation', and it's mandatory for the
> compiler to elide the move/copy operation.
>
> But let's change it a little:
>
> std::mutex Func(void)
> {
> std::mutex mtx;
> return mtx;
> }
>
> Now it no longer compiles. What we have here is 'Named Return Value
> Optimisation'. In this circumstance, the compiler may elide the
> move/copy operation if it wants to, but still the move/copy
> constructor must be accessible -- therefore it won't work with an
> std::mutex.
>
> So this begs the question.... Is it at all possible to return a locked
> mutex by value from a function?

Here's a better question: is that a thing you should want? There are
good, reasonable reasons to want to be able to return non-moveable
objects. This is not one of them. Indeed, this is precisely the sort
of thing that people make objects non-moveable to *prevent* people
from doing.

There have been discussions of the right sort of syntax for this
functionality. P2025 went into some detail on the matter, but it
hasn't moved forward in a couple of years. From
https://github.com/cplusplus/papers/issues/756 it seems the committee
was not impressed by the idea.

I doubt that returning locked mutexes is the kind of example that
would change their minds.


------------------------------

Message: 4
Date: Sun, 16 Jul 2023 02:41:16 +0200
From: Magnus Fromreide <magfr_at_[hidden]>
To: Marcin Jaczewski via Std-Proposals
        <std-proposals_at_[hidden]>
Subject: Re: [std-proposals] Impact of defaulted ctor on value
        initialization
Message-ID: <ZLM8rHJwsxThYv6R_at_[hidden]>
Content-Type: text/plain; charset=utf-8

On Tue, Jul 11, 2023 at 05:34:24PM +0200, Marcin Jaczewski via Std-Proposals wrote:

I just wanted to throw in basic_string::resize_and_overwrite as that also
tries to basically solve this very same problem or at least another aspect
of it.

With this proposal one could write a nonmember resize_and_overwrite as

void resize_and_overwrite(string& s, size_type count, Operation op)
{
  s.resize(count + 1, default_init);
  s.resize(std::move(op)(s.data(), count));
}

/MF

> wt., 11 lip 2023 o 16:36 Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]> napisa?(a):
> >
> > On Tue, Jul 11, 2023 at 9:22?AM Marcin Jaczewski <marcinjaczewski86_at_[hidden]> wrote:
> >>
> >> wt., 11 lip 2023 o 14:52 Arthur O'Dwyer via Std-Proposals <std-proposals_at_[hidden]> napisa?(a):
> >> >
> >> > The root problem here IMHO is that C++ has two core ideas ("default-initialization" and "value-initialization") which are distinguishable in the Platonic realm; but the syntax corresponding to these ideas is all conflated together. Specifically two flaws:
> >> >
> >> > (1) Some primitive types (like `int`) make default-initialization behave differently from value-initialization. It is possible to emulate this behavior in a user-defined class type, but only under very specific restrictions that correspond to leaving specific fields uninitialized at the machine level. So for example if I wanted to define a `class TrappingInt` such that
> >> > auto i1 = int(); // value-initialized to zero
> >> > auto i2 = TrappingInt(); // value-initialized to zero
> >> > int i1; // default-initialized to garbage
> >> > TrappingInt i2; // default-initialized to a trapping value
> >> > well, there's no way to achieve that in today's C++. This is because the syntax for defining a "default constructor" really does double duty: it defines the default constructor and the, let's say, "value constructor," simultaneously, in an implicit and entangled way. This is very convenient for most programmers, but specifically inconvenient for the rare programmer who wants to emulate `int` as shown above. It means that there is behavior of the primitive types that can't be emulated by user-defined types.
> >> >
> >> > (2) Suppose I want to construct a prvalue of a given type, using its {default,value} constructor. There is no core-language syntax for this at all.
> >> > auto k1 = int(); // OK, value-initialize an int
> >> > auto k2 = ???; // inexpressible: default-initialize an int
> >> > IIRC, we recently discussed this problem w.r.t. -ftrivial-var-init=zero and prvalues of types like `std::array`: C++'s syntax lets us write
> >> > void compute(std::array<int, 1000> scratch_space) { ... immediately overwrite whatever's in scratch_space ... }
> >> > void test() { compute(std::array<int, 1000>()); }
> >> > but that wastefully zeroes out 4000 bytes of memory before calling `compute`. What we actually wanted to express here was "pass a default-initialized std::array to `compute`," but C++ doesn't give us any syntax to express that.
> >>
> >> And this have another problem that `int i;` can be source of UB if was
> >> done unintentionally, for me we could fix both problems by introducing
> >> new keyword similar to `nullptr`,
> >> like `default_init` or something like this, then we could make:
> >>
> >> class F
> >> {
> >> int i;
> >> F(std::default_init_t) = default; //make default init for `i`
> >> F() = default; // value init for `i`
> >> };
> >>
> >> int main()
> >> {
> >> int i[10] = default_init; // same as `int i[10];`
> >> std::array<F, 10> f = { default_init }; // or `std::array<F, 10> f
> >> = default_init;`
> >
> >
> > Hmm, normally aggregate-initialization zero-fills the unspecified values. So
> > int a[10] = default_init;
> > would mean "garbage-fill the entire array object," but
> > int a[10] = {default_init};
> > would mean "garbage-fill a[0], and then zero-fill a[1] through a[9]." Right? This is fine, but awfully subtle.
> > Likewise,
> > std::array<F, 10> f = { default_init };
> > std::array<F, 10> f = default_init;
> > would presumably do different things, not the same thing (as you implied).
> >
>
> In general case yes, but if `array` is "default aware" it would have
> the same effect.
> First line `f = { default_init };` do not need any changes in `array`
> and could work right after language change,
> similar like in some random struct:
> ```
> {
> default_init,
> 13,
> default_init,
> }
> ```
> we can partially aggregate initialization with some uninitialized members.
>
> >> Interesting thing will be when we do something like:
> >>
> >> class SafeInt {
> >> int i;
> >> explicit SafeInt(std::default_init_t) = default;
> >> };
> >> int main() {
> >> SafeInt i; //error, it uses explicit constructor!
> >> SafeInt j = SafeInt(default_init); //ok, no initialization is done
> >> };
> >
> >
> > This syntax seems suboptimal to me. I observe:
> > - `SafeInt i;` is not an implicit conversion; it's perfectly well allowed to use an `explicit` constructor. So `explicit` isn't the keyword you mean, here. What `explicit` disallows is `SafeInt i = {};` ? the implicit [scare-quotes] "conversion" of `{}` to an expression of type `SafeInt`.
>
> Ok, probaby bit too much for "overload" for explicit meaning.
>
> > - Your use-case, IIUC, wants to forbid `SafeInt i;` but explicitly permit `SafeInt j = SafeInt(default_init)`. Presumably you'd also like to permit `SafeInt j = default_init`, since it's equally explicit about what's happening there. This smells more like a case for a tag type than anything specifically related to default-initialization. Like, today, we could say
> >
>
> Yes
>
> > struct default_init_t { explicit default_init_t() = default; };
> > inline constexpr default_init_t default_init;
> >
> > class SafeInt {
> > public:
> > SafeInt(default_init_t) {}
> > SafeInt(int i) : i_(i) {}
> > private:
> > explicit SafeInt() = default;
> > int i_;
> > };
> > SafeInt si1; // ill-formed
> > SafeInt si2 = 0; // OK
> > SafeInt si3 = default_init; // OK, i_ is garbage
> > SafeInt si4 = SafeInt(default_init); // OK, i_ is garbage
> > SafeInt si5 = SafeInt(); // ill-formed (!!)
> > void f6(SafeInt); ... f6(default_init);
> >
> > The two problems I see with today's solution are:
> > - si5 is ill-formed
> > - We would like si3 to be well-formed but f6 to be ill-formed; we can't get that without a core-language change, since both are syntactically implicit conversions
> >
> > Your idea, to make `default_init` a keyword with semantics different from the above `::default_init`, clearly has the potential to help with one or both of these problems;
>
> Yes, as `::default_init` can work only with user types but can't with
> builtin ones (at least not easily). As this is a special type, we can
> specify how we will handle all corner cases and even add some
> exceptions for better user experience. Like it will work by default
> for any type, even if it does not explicitly declare use of it.
> Another thing is that compiler can better interact with it as know
> intention and semantic of this new keyword.
>
> > but I don't think it quite gets there yet, does it?
> >
> > ?Arthur
>
> Its possible, for now, is more analogous to `nullptr` functionality.
> But I see potential in something like this.
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals

--
Magnus Fromreide                                        +46-13 17 68 48
Tornhagsv?gen 24, 2tr                                   magfr_at_[hidden]
SE-582 37  LINK?PING
------------------------------
Subject: Digest Footer
Std-Proposals mailing list
Std-Proposals_at_[hidden]
https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
------------------------------
End of Std-Proposals Digest, Vol 52, Issue 57
*********************************************

Received on 2023-07-16 02:40:00