PxxxxR1
PRvalue Parameters

New Proposal,

This version:
http://virjacode.com/papers/prvalue_params001.htm
Latest version:
http://virjacode.com/papers/prvalue_params.htm
Author:
Thomas PK Healy <healytpk@vir7ja7code7.com> (Remove all sevens from email address)
Audience:
SG17, SG18
Project:
ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21

Abstract

This proposal introduces a new feature to the C++ language, allowing functions to accept PRvalues as arguments.

1. Introduction

Functions can be overloaded to accept different types of arguments, including Lvalues and Rvalues. However, it is currently not possible to specify a function that accepts only a PRvalue. A function with a parameter type such as int && can take both an XRvalue and a PRvalue, however this proposal aims to introduce new syntax, int &&&, to indicate that a function can only accept a PRvalue as an argument.

2. Motivation

2.1. optional<T>::emplace

Consider the std::optional class in the C++ standard library, which provides a way to represent optional values. It includes the emplace method that constructs the contained object in-place. With the proposed feature, we can extend the functionality of std::optional<T>::emplace to have an overload which only accepts a PRvalue argument, which would allow us to emplace an unmovable-and-uncopyable type (such as std::mutex). This allows for more flexibility and efficiency in code, as illustrated in the following example:

extern int some_global_variable;

extern std::mutex FuncReturnsMutexByValue(int, int, int);

int main(void)
{
    optional<std::mutex> om;
    om.emplace(  FuncReturnsMutexByValue(1, 2, some_global_variable)  );
}

In this example, std::optional<mutex>::emplace is invoked with a PRvalue argument returned from the function FuncReturnsMutexByValue. The compiler determines the appropriate overload of std::optional<mutex>::emplace based on the argument type, allowing seamless integration of PRvalue parameters. Note that the arguments passed to FuncReturnsMutexByValue, i.e. 1, 2, some_global_variable, are evaluated before the invocation of the emplace method, however the actual invocation of FuncReturnsMutexByValue is delayed until explicitly requested inside the implementation of the emplace method.

3. Possible Implementation

The implementation of the proposed feature involves changes to the C++ core language to recognise and handle the &&& syntax. It also requires updates to the standard library to support functions and methods that accept PRvalue parameters.

3.1. optional<T>::emplace

template<typename T>
T &optional<T>::emplace(T &&&arg)
{
    this->reset();
    T &retval = *::new(this->buffer) T( arg() );
    this->has_value_bool = true;
    return retval;
}

In the above implementation, you can see that the argument, arg, behaves as though it were a functor object. The invocation of arg() results in the invocation of FuncReturnsMutexByValue. Note however that the arguments passed to FuncReturnsMutexByValue, i.e. 1, 2, some_global_variable, were evaluated before the invocation of the emplace method.

3.2. x86_64 Linux, Apple, FreeBSD

Let’s give consideration to the following program which uses the new feature proposed in this paper:

#include <cstdint>    // uint64_t
#include <atomic>     // atomic<T>
#include <new>        // "placement new"

using std::atomic, std::uint64_t;

atomic<uint64_t> FuncReturnsAtomicByValue(uint64_t const a, uint64_t const b)
{
    return a + b + 0x7f;
}

template<typename T>
class MyOptional {
    alignas(T) char unsigned buf[sizeof(T)];
    bool has_value_bool = false;
public:
    template<typename Param>
    T &emplace(Param &&&arg)
    {
        T *const p = static_cast<T*>(static_cast<void*>(buf));
        if ( this->has_value_bool )
        {
            this->has_value_bool = false;
            p->~T();
        }
        ::new(p) T( arg() );
        this->has_value_bool = true;
        return *p;
    }
};

uint64_t GiveMeFive(void) { return 5u; }

int main(void)
{
    MyOptional< std::atomic<uint64_t> > var;
    var.emplace(  FuncReturnsAtomicByValue(4u, GiveMeFive())  );
}

Let’s focus on the following line inside main:

var.emplace(  FuncReturnsAtomicByValue(4u, GiveMeFive())  );

And now let’s write the x86_64 assembler for this line. Note that on x86_64 for Linux, Apple and FreeBSD, the first three function arguments go in the registers RDI, RSI, RCX, and the return value goes in RAX. The first thing that happens is that we evaluate all of the arguments to FuncReturnsAtomicByValue and we push them onto the stack right-to-left:

call GiveMeFive
push rax         ; push return value from GiveMeFive onto stack
push 4           ; push 4 onto stack

The compiler must output a separate helper function which invokes FuncReturnsAtomicByValue after moving the arguments from the stack into the registers. Note that the address of the return slot is in RDI.

Helper_Function:
    mov rax, rsi            ; address of arguments in memory
    mov rsi, [rax+0]        ; move 4 from memory to register
    mov rcx, [rax+8]        ; move 5 from memory to register
    jmp FuncReturnsAtomicByValue

The method emplace accepts a PRvalue parameter, which is implemented as a function which takes three parameters:

Really it’s as though the emplace method had been written:

T &std::optional<T>::emplace( T (*helper_func)(void*), void *args )
{
    T *const p = static_cast<T*>(static_cast<void*>(buf));
    if ( this->has_value_bool )
    {
        this->has_value_bool = false;
        p->~T();
    }
    ::new(p) T( helper_func(args) );
    this->has_value_bool = true;
    return *p;
}

And so then the C++ code inside main becomes the following assembler:

call GiveMeFive
push rax                  ; push return value from GiveMeFive onto stack
push 4                    ; push 4 onto stack
mov rdi, var              ; set 1st argument to address of var
mov rsi, Helper_Function  ; set 2nd argument to address of Helper_Function
mov rcx, rsp              ; set 3rd argument to current stack pointer
call emplace

4. Design considerations

Introducing PRvalue parameters into the language may have implications for existing codebases. While the addition of this feature does not render any existing code ill-formed, it may affect how functions and methods are called and overloaded. Careful consideration should be given to backward compatibility and potential conflicts with existing code that relies heavily on function parameter matching.

Additionally, the introduction of PRvalue parameters may require adjustments to coding conventions and best practices to ensure consistent and efficient usage across codebases. Documentation and educational materials should be updated to reflect the new syntax and its implications on function parameter passing.

5. Revision history

R0 => R1

6. Acknowledgements

For their feedback and contributions on the mailing list std-proposals@lists.isocpp.org:

    Gašper Ažman, Thiago Macieira, Matthew Taylor