C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Return Value Optimisation whenever you need it (guaranteed elision)

From: Frederick Virchanza Gotham <cauldwell.thomas_at_[hidden]>
Date: Sun, 16 Jul 2023 16:39:38 +0100
I reply to Jason and Arthur in series below.


On Sun, Jul 16, 2023 at 4:15 PM Jason McKesson wrote:
>
> Frederick Virchanza Gotham wrote:
> > Next I will combine this code with my previous code so that we can:
> > Step 1 : Return a locked mutex by value from a function
> > Step 2 : Have that locked mutex placed inside a global std::optional variable
>
> That's neat and all, but that doesn't really address the core issues.
> Nobody claimed that the functionality was unimplementable, so showing
> a way of doing it through inline assembly isn't really addressing any
> of the things people are talking about.



Yeah but if I implement it in inline assembly for System V x86_64,
Microsoft x64, cdecl, stdcall, aarch64, arm32, then I think it will
get a lot more attention when people see how easily these
implementations can simply slide into the C++26 standard library
without any changes to the core language.



On Sun, Jul 16, 2023 at 4:03 PM Arthur O'Dwyer wrote:
>
> You still can't get such an object into a std::list<mutex>, or even into a variable of
> type `mutex` on the stack. (You say "it's" totally safe to do, but you seem to have
> locked onto the wrong "it.")


That sounds like a dare. Give me a few days and I'll have an
"std::list<std::mutex>" populated with locked mutexes that were
returned by value from functions (or functors!).

Anyway I have this all working now, I have a function returning a
locked mutex by value, and I have it placed inside a global
std::optional. I've gotten stuff done on Sunday's before and all but
today was an absolute corker.

This code works entirely with 'std::function' instead of function
pointers, so you can also use lambdas-with-captures or functor
objects.

Here it is up on GodBolt:

    https://godbolt.org/z/sfdTPq9GK

I've done enough today, I'm going to the park.

And here it is copy-pasted:

#include <functional> // function
#include <new> // placement 'new'
#include <optional> // optional
#include <type_traits> // remove_cvref_t

// Providing forward-declaration of class
// so that Invoker can make it a friend
template<typename T>
class PutRetvalInOptional;

namespace detail {

    template<typename T, typename... Params>
    class Invoker {

        // The constructor of this class is private so that you can't just pull
        // one out of thin air. There is only one friend: PutRetvalInOptional<T>

        // An object of type 'Invoker<T,Params...>' is returned
        // by value from PutRetvalInOptional<T>::operator()<Params...>

        std::optional<T> *const popt = nullptr;
        std::function<T(Params...)> const &f;

        Invoker(std::optional<T> &argO, std::function<T(Params...)>
const &argF) : popt(&argO), f(argF) {}

    public:
        void operator()(Params... args) /* deliberately non-const */
        {
            // 'std::optional<T>' inherits privately from
'std::_Optional_base<T>'
            // We need access to the base class to get/set '_M_value'
and '_M_engaged'.
            auto &base =
*static_cast<std::_Optional_base<T>*>(static_cast<void*>(popt));

            // The next four lines take advantage of a feature of the
            // System V x86_64 calling convention. The return value
            // can be placed as the first parameter (because both
            // use the RDI register).
            using F = std::function<T(Params...)>;
            using FuncPtr = void (*)(T*, F const *, Params...);
            auto const p = reinterpret_cast<FuncPtr>( &F::operator() );
            p( &base._M_payload._M_payload._M_value, &f,
std::forward<Params>(args)... );

            // Lastly we need to manually set the 'has_value()' of the
'std::optional' to 'true'
            base._M_payload._M_engaged = true;
        }

        Invoker(Invoker &&) = delete;
        Invoker(Invoker const & ) = delete;
        Invoker &operator=(Invoker &&) = delete;
        Invoker &operator=(Invoker const & ) = delete;

        friend class PutRetvalInOptional<T>;
    };
} // close namespace 'detail'

template<typename T>
class PutRetvalInOptional {
    std::optional<T> *const popt;
public:
    PutRetvalInOptional(std::optional<T> &arg) : popt(&arg) {}

    template<typename... Params>
    detail::Invoker<T,Params...>
operator()(std::function<T(Params...)> const &f)
    {
        return detail::Invoker<T,Params...>(*popt, f);
    }

    PutRetvalInOptional(PutRetvalInOptional &&) = delete;
    PutRetvalInOptional(PutRetvalInOptional const & ) = delete;
    PutRetvalInOptional &operator=(PutRetvalInOptional &&) = delete;
    PutRetvalInOptional &operator=(PutRetvalInOptional const & ) = delete;
};

extern "C" void InvokeFunctional(void *const p,
std::function<void(void*)> const &f)
{
    // I could have just written this function as
    // a few lines of assembler, but I didn't want
    // to have to worry about the mangling of:
    // std::function<void(void*)>::operator()

    if ( f ) f(p);
}

extern "C" void rvo_guaranteed_detail(void);

__asm("rvo_guaranteed_detail: \n"
      ".intel_syntax noprefix \n"
      " push rdi \n" // save to restore later
      " push rdx \n" // save to restore later
      " call InvokeFunctional \n" // call 'create'
      " pop rdx \n" // restore after call
      " pop rdi \n" // restore after call
      " mov rsi, rdx \n" // Move 3rd argument to 2nd
      " call InvokeFunctional \n" // call 'manipulate'
      " ret \n"
      ".att_syntax");

template<typename T>
std::remove_cvref_t<T> rvo_guaranteed(
    std::function<void(std::remove_cvref_t<T>*)> create,
    std::function<void(std::remove_cvref_t<T>*)> manipulate )
{
    auto const f = reinterpret_cast< decltype(&rvo_guaranteed<T>) >(
rvo_guaranteed_detail );
    return f(create,manipulate);
}

// ===================== Here comes the test code:
==========================================

#include <iostream> // cout
using std::cout, std::endl;

struct Mutex {
    // ---------- cannot move and cannot copy ----------
    Mutex(Mutex &&) = delete;
    Mutex(Mutex const & ) = delete;
    Mutex &operator=(Mutex &&) = delete;
    Mutex &operator=(Mutex const & ) = delete;

    Mutex(void) { cout << "construct\n"; }
    void lock(void) { cout << "lock\n" ; }
    void unlock(void) { cout << "unlock\n" ; }
    ~Mutex(void) { cout << "destroy\n" ; }
};

Mutex GiveMeLockedMutex(int const a, double const b, char const *const
p) // dummy parameters
{
    cout << "Args: " << a << " " << b << " " << p << endl;

    return rvo_guaranteed<Mutex>(
        [ ](Mutex *const p) { ::new(p) Mutex(); },
        [a](Mutex *const p) { if ( a % 2 ) p->lock(); } );
}

std::optional<Mutex> myglobal;

int main(int const argc, char **const argv)
{
    std::function< Mutex(int,double,char const *) > const stdfunc =
GiveMeLockedMutex;

    PutRetvalInOptional(myglobal)(stdfunc)(argc,4.2,"Hello World");

    // GiveMeMutex has returned a locked mutex by value
    // ...and that mutex is now inside a global optional

    myglobal->unlock();
}

Received on 2023-07-16 15:39:47