C++ Logo

std-proposals

Advanced search

Re: [std-proposals] std::elide

From: Breno Guimarães <brenorg_at_[hidden]>
Date: Wed, 22 May 2024 10:39:30 -0300
I mistakenly cut part of my response. The
"getMutexTheOtherTeamToldMeToUse()" could be an application wide function
that your project needs to use for logging/tracking.
With that, you cannot forget to call the post-initialization. It's done for
you.



On Wed, May 22, 2024 at 10:36 AM Breno Guimarães <brenorg_at_[hidden]> wrote:

> I think you drilled too much into the "mutex" and "locked" aspects, rather
> than thinking of the immovable and post initialization.
>
> You could have
> void foo() {
> static auto mutex = getMutexTheOtherTeamToldMeToUse();
> }
>
> There are even other types that *are* movable but moving is not cheap.
>
>
> On Wed, May 22, 2024 at 10:17 AM Tiago Freire via Std-Proposals <
> std-proposals_at_[hidden]> wrote:
>
>> While I can understand RVO, mutexes has got to be the worst possible
>> example for an application of this.
>> It should just simply never happen in real code ever.
>> I have posed this question before but got ignored, but it is crucial to
>> the point.
>>
>> Why std::mutex can not be copied or moved (copy constructed or move
>> constructed)?
>> The reason for this is that some implementations of mutexes (posix
>> mutexes are in this category), the object representation of the mutex
>> itself is a piece of memory that lives in the user's space.
>> And std::mutex accommodates this implementation.
>> The exact address of this object is what makes a specific mutex that
>> exact mutex.
>> Moving or copying implies at least 2 distinct objects at different
>> addresses, while in windows because it is implemented via a system handle
>> you would be able to transfer the mutex representation, but not on posix.
>> Copying data from one address onto another would change the state of the
>> mutex that is being copied to, but it wouldn't be able to make the new
>> mutex be the old mutex, because it is the address that defines the identity
>> of the mutex, not the data.
>>
>> I'm not even mention the implications of moving a mutex object before
>> destroying while it is potentially in use elsewhere.
>>
>>
>> So, what you are proposing could only ever work like this:
>>
>> std::mutex m = getLockedMutex();
>>
>> and in the exclusive condition that RVO is taking place.
>> If it doesn't the function itself would be ill-formed and so would the
>> following code:
>>
>>
>> std::mutex m;
>> m = getLockedMutex();
>>
>>
>> Ok, that's great, but why do you need to return a locked mutex again?
>> Even without knowing what else it might be doing, assuming that the system
>> in place was design sensibly, what is the bare minimum that we can deduce
>> that this function must be doing.
>>
>> 1. It makes absolutely no sense to create a mutex if you are not using
>> it. The bare minimum there must be another thread that uses it to
>> synchronize with it.
>> 2. And why would you need to return it locked? You would only do that if
>> the mutex is protecting something, and it is locked because you don't want
>> other threads accessing it. It means that other threads are already using
>> it even before the function returns. If not, you could have just told the
>> caller when to lock instead.
>> 3. The mutex lives on the stack, specifically aligned at the point of
>> where the caller as set out for it. So, whatever it is that the other
>> thread is doing with this mutex, it must be resolved before this piece of
>> code goes out of scope and the mutex is destroyed.
>> 4. Because the mutex lives on the stack aligned at the point of the
>> caller, but it is written in a way that the callee must be the one
>> constructing this mutex. It is not like it has the option of choosing
>> between mutex A or mutex B, the function needs to initialize this exact
>> mutex at the caller's address and it can not return without valid
>> constructed mutex.
>> 5. The exact address of where in the stack this mutex resides is not
>> easily predictable, so I don't think it would be sensible to do it by means
>> other than reading the stack pointer at runtime. Otherwise, it couldn't
>> make this specific mutex known to other threads.
>>
>> Even without knowing anything else about what this function is doing,
>> this must be true:
>> 1. The function must assume that the caller has prepared an address for
>> the mutex
>> 2. The function must (it is not optional) initialize the mutex in the
>> memory space prepared by the caller
>> 3. The function must complete the initialization before making it know to
>> another user thread, and lock it (the order of making it known to another
>> thread first or locking the mutex, which comes first is not determined)
>> 4. And these must happen within the function before returning.
>>
>> Given that all of this must be set by the caller. There's absolutely no
>> reason why the caller wouldn't want to just pass an already initialized
>> mutex to the function instead.
>> Even without knowing about anything else that this function might be
>> doing.
>>
>> This:
>> std::mutex m;
>> getLockedMutex(m);
>>
>> Should have been the design.
>> And all of this discussion about changing the language to accommodate
>> returning a locked mutex, it just makes absolutely no sense from a
>> principled design perspective.
>> It's a bad example for RVO.
>>
>> It is trying to jump through hoops that have no reason to be jumped.
>>
>>
>> -----Original Message-----
>> From: Std-Proposals <std-proposals-bounces_at_[hidden]> On Behalf
>> Of Frederick Virchanza Gotham via Std-Proposals
>> Sent: Wednesday, May 22, 2024 12:04
>> To: std-proposals_at_[hidden]
>> Cc: Frederick Virchanza Gotham <cauldwell.thomas_at_[hidden]>
>> Subject: Re: [std-proposals] std::elide
>>
>> On Wed, May 22, 2024 at 4:00 AM Arthur O'Dwyer wrote:
>> >
>> > auto getLockedMutex(std::mutex *pm) {
>> > Auto(pm->lock());
>> > return std::mutex();
>> > }
>> >
>> > int main() {
>> > std::mutex m = getLockedMutex(&m);
>> > m.unlock();
>> > }
>>
>>
>> I had to write this out step by step to understand it. So we start with:
>>
>> auto getLockedMutex(std::mutex *pm)
>> {
>> Auto(pm->lock());
>> return std::mutex();
>> }
>>
>> which is equivalent to:
>>
>> std::mutex getLockedMutex(std::mutex *pm)
>> {
>> Auto(pm->lock());
>> return std::mutex();
>> }
>>
>> which is equivalent to:
>>
>> std::mutex getLockedMutex(std::mutex *pm)
>> {
>> using std::mutex;
>> struct S { mutex &m; S(mutex &arg) : m(arg) {} ~S(){ m.lock(); }
>> } s(*pm);
>> return mutex();
>> }
>>
>> which is equivalent on x86_64 and most other platforms to:
>>
>> void getLockedMutex(std::mutex *arg0, std::mutex *arg1)
>> {
>> using std::mutex;
>> struct S { mutex &m; S(mutex &arg) : m(arg) {} ~S(){ m.lock(); }
>> } s(*arg1);
>> ::new(arg0) std::mutex;
>> }
>>
>> To be sure, I checked the assembler produced on GodBolt. The first and
>> last function start off identically:
>>
>> push rbx
>> mov rbx, rdi
>> xorps xmm0, xmm0
>> movups xmmword ptr [rdi + 16], xmm0
>> movups xmmword ptr [rdi], xmm0
>> mov qword ptr [rdi + 32], 0
>> mov rdi, rsi
>> call pthread_mutex_lock_at_PLT
>> test eax, eax
>> jne Something_Went_Wrong
>> mov rax, rbx
>> pop rbx
>> ret
>> Something_Went_Wrong:
>> mov edi, eax
>> call std::__throw_system_error(int)@PLT
>>
>> But the last function has two more instructions tagged onto the end:
>>
>> mov rdi, rax
>> call __clang_call_terminate
>>
>> The difference is that in Arthur's original function, if "lock"
>> throws, then the mutex must get destroyed before the function returns
>> -- so then I looked through the assembler trying to find the call to the
>> destructor. . . but I couldn't find it (then I realised that
>> "mutex::~mutex" doesn't do anything). But there's another difference .
>> . . if "lock" throws in Arthur's code, then std::terminate is called
>> because an exception is thrown in the destructor of a local object, as
>> demonstrated here:
>>
>> https://godbolt.org/z/xW1Y7hW4G
>>
>> (By the way I suggested to Arthur in an email a few months ago that he
>> should enclose the destructor of Auto inside a "try/catch(...)").
>>
>> Let's see what happens though with a class type that has a destructor
>> with an observable effect. So let's start off with:
>>
>> struct S {
>> inline static volatile int n = 0;
>> S(S const & ) = delete;
>> S(S &&) = delete;
>> S() __attribute__ ((noinline)) { n = 1; }
>> ~S() __attribute__ ((noinline)) { n = 2; }
>> void Throw(void) noexcept(false) __attribute__ ((noinline)) { if
>> ( 666 != n ) throw n; }
>> };
>>
>> And then we have Arthur's style of function:
>>
>> S Func(S *p)
>> {
>> struct X { S &s; X(S &arg) : s(arg) {} ~X(){ s.Throw(); } } x(*p);
>> return S();
>> }
>>
>> And then we have my own style of function:
>>
>> void Func2_(S *arg0, S *arg1)
>> {
>> struct X { S &s; X(S &arg) : s(arg) {} ~X(){ s.Throw(); } }
>> x(*arg1);
>> ::new(arg0) S();
>> }
>>
>> Arthur's style becomes:
>>
>> push r14
>> push rbx
>> push rax
>> mov r14, rsi
>> mov rbx, rdi
>> call S::S() [base object constructor]
>> mov rdi, r14
>> call S::Throw()
>> mov rax, rbx
>> add rsp, 8
>> pop rbx
>> pop r14
>> ret
>> mov rdi, rax
>> call __clang_call_terminate
>>
>> While my own style becomes:
>>
>> push rbx
>> mov rbx, rsi
>> call S::S() [base object constructor]
>> mov rdi, rbx
>> call S::Throw()
>> pop rbx
>> ret
>> mov rdi, rax
>> call __clang_call_terminate
>>
>> Neither of them invoke the destructor of S. If we were to edit Arthur's
>> style to manually invoke the destructor as follows:
>>
>> S Func1(S *p)
>> {
>> struct X {
>> S &s;
>> X(S &arg) : s(arg) {}
>> ~X(void)
>> {
>> try { s.Throw(); }
>> catch(...) { s.~S(); throw; }
>> }
>> } x(*p);
>> return S();
>> }
>>
>> Then the assembler becomes:
>>
>> push r14
>> push rbx
>> push rax
>> mov rbx, rsi
>> mov r14, rdi
>> call S::S() [base object constructor]
>> mov rdi, rbx
>> call S::Throw()
>> mov rax, r14
>> add rsp, 8
>> pop rbx
>> pop r14
>> ret
>> mov rdi, rax
>> call __cxa_begin_catch
>> mov rdi, rbx
>> call S::~S() [base object destructor]
>> call __cxa_rethrow
>> mov rdi, rax
>> call __clang_call_terminate
>>
>> This one is a little better as we can see the destructor getting called,
>> but when we try to re-throw the exception, std::terminate gets called. So
>> this isn't exception-safe.
>>
>> Of all the possible ways of achieving NRVO shared so far on this mailing
>> list in order to return a locked mutex by value from a function, I think
>> the best one so far is as follows: Write a function that returns a
>> Derived<T> instead of a T, and then inside the constructor of Derived<T>,
>> lock the mutex. Then take the address of the following function:
>> Derived<T> Func(void);
>> and cast it to:
>> T (*)(void);
>> Invoke the function pointer, and voila you get back a locked mutex.
>> Maybe put in a few static_assert's to make sure that Derived<T> is
>> equivalent in size and alignment to T.
>>
>> Or of course there's always the idea of giving us full access to the
>> return slot: http://www.virjacode.com/papers/returnslot.htm
>> --
>> 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
>>
>

Received on 2024-05-22 13:39:43