PxxxxR0
std::elide

New Proposal,

This version:
http://virjacode.com/papers/elide000.htm
Latest version:
http://virjacode.com/papers/elide.htm
Author:
TPK 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

Add a new function to the standard library to make possible the emplacement of an unmovable-and-uncopiable prvalue into types such as std::optional, std::variant, std::any -- without requiring an alteration to the definitions of these classes.

Note: A small change is required to the core language.

1. Introduction

While it is currently possible to return an unmovable-and-uncopiable class by value from a function:

std::counting_semaphore<8> FuncReturnsByValue(unsigned const a, unsigned const b)
{
    return std::counting_semaphore<8>(a + b);
}

It is not possible to emplace this return value into an std::optional:

int main(){    std::optional< std::counting_semaphore<8> > var;   
var.emplace( FuncReturnsByValue(1,2) ); // compiler error}

This paper proposes a solution to this debacle, involving an addition to the standard library, along with a small change to the core language.

2. Motivation

2.1. emplace( FuncReturnsByValue() )

There is a workaround to make this possible, and it is to use a helper class with a conversion operator:

int main(){    std::optional< std::counting_semaphore<8> > var;   
struct Helper { operator std::counting_semaphore<8>() { return FuncReturnsByValue(1,2); } };
var.emplace( Helper() );}

This is possible because of how the emplace member function is written:

template<typename... Params>T &emplace(Params&&... args){    . . .    ::new(buffer) T( forward<Params>(args)... );    . . .}

The compiler cannot find a constructor for T which accepts a sole argument of type Helper, and so it invokes the conversion operator, meaning we effectively have:

::new(buffer) T( FuncReturnsByValue(1,2) );

In this situation, where we have a prvalue returned from a function, we have guaranteed elision of a copy/move operation. This proposal aims to simplify this technique by adding a new function to the standard library called std::elide which can be used as follows:

int main(){    std::optional< std::counting_semaphore<8> > var;   
var.emplace( std::elide(FuncReturnsByValue,1,2) );}

3. Possible implementation

#include <cstddef>      // size_t
#include <functional>   // invoke
#include <tuple>        // get, tuple, tuple_element_t, apply
#include <type_traits>  // conditional, is_lvalue_reference, is_same, remove_cvref_t, remove_reference
#include <utility>      // index_sequence, make_index_sequence, move, forward

namespace std {
    // The following function 'apply_perfectly' is required
    // because 'apply' has a flaw. The flaw is that 'apply'
    // cannot be used with a tuple that contains an Rvalue
    // reference. The 'static_cast' ensures that the Rvalue
    // references are not passed as Lvalues.
    template<typename F, typename Tuple, size_t... I>
    constexpr decltype(auto) apply_perfectly_impl(F &&f, Tuple &&t, index_sequence<I...>)
    {
        return invoke( forward<F>(f),
          static_cast< tuple_element_t< I, remove_cvref_t<Tuple> > >(
            get<I>(  forward<Tuple>(t)  )
          )... );
    }

    template <typename F, typename Tuple>
    constexpr decltype(auto) apply_perfectly(F &&f, Tuple &&t)
    {
        return apply_perfectly_impl(
            forward<    F>(f),
            forward<Tuple>(t),
            make_index_sequence< tuple_size_v< remove_cvref_t<Tuple> > >{});
    }

    template<typename R, typename F_ref, typename... Params_refs>
    requires is_same_v< R, remove_reference_t<R> > // To ensure F returns by value
    class elide_t final {
        using F = remove_reference_t<F_ref>;
        F &&f;
        tuple< Params_refs... > const args_tuple;  // just a tuple full of references
        explicit elide_t(F &&arg, Params_refs... args) noexcept
          : f( move(arg) ),
            args_tuple( static_cast<Params_refs>(args)... ) {}
    public:
        operator R(void)
        {
            return apply_perfectly( static_cast<F_ref>(f), args_tuple );
        }
        template<typename F2, typename... Params2> friend auto elide( F2&&, Params2&&... ) noexcept;
    };

    template<typename F, typename... Params>
    auto elide(F &&f, Params&&... args) noexcept
    {
        return elide_t<
                 decltype( invoke( forward<F>(f), forward<Params>(args)... ) ),
                 conditional_t< is_lvalue_reference_v<     F>,      F&,      F&& >,
                 conditional_t< is_lvalue_reference_v<Params>, Params&, Params&& >...
               >( move(f), forward<Params>(args)... );
    }
}

4. Design considerations

4.1. template constructor

The above implementation of std::elide will not work in a situation where the class T has a constructor which accepts a specialisation of the template class std::elide_t as its sole argument, such as the following AwkwardClass:

class AwkwardClass {
    std::mutex m;  // cannot move, cannot copy
public:
    template<typename T>
    AwkwardClass(T &&arg) noexcept
    {
        std::cout << typeid(T).name() << std::endl;
    }
};

AwkwardClass ReturnAwkwardClass(int const arg)
{
    return AwkwardClass(arg);
}

int main()
{
    std::optional<AwkwardClass> var;
    var.emplace( std::elide(ReturnAwkwardClass, -1) );
}

The problem here is that this will result in the invocation of the constructor of AwkwardClass with the template parameter type T set to a specialisation of std::elide_t. A workaround here is to apply a constraint to the constructor of AwkwardClass as follows:

template<typename T>requires (!std::is_specialization_v<std::remove_cvref_t<T>, std::elide_t>)AwkwardClass(T &&arg) noexcept{    std::cout << typeid(T).name() << std::endl;}

In order that class definitions do not have to be altered in order to apply this constraint to template constructors, this proposal makes a change to the core language to prevent the constructor of any class type from having a specialisation of std::elide_t as any of its parameter types.

5. Proposed wording

The proposed wording is relative to [N4950].

In subclause 11.4.5 [class.ctor], append a paragraph:

9   A constructor shall not have any parameter type which is a specialization of std::elide_t. A template constructor, after the substitution of type parameters, shall not have any parameter type which is a specialization of std::elide_t.

6. Impact on the standard

This proposal is a small library extension combined with a small change to the core language. The change to the core language is one short paragraph to be added to Section 11.4.5 [class.ctor]. The text addition is 35 words, and the addition has no effect on any other part of the standard.

7. Impact on existing code

No existing code becomes ill-formed. The behaviour of all exisiting code is unaffected by this addition to the standard library.

References

Normative References

[N4950]
Thomas Köppe. Working Draft, Standard for Programming Language C++. 10 May 2023. URL: https://wg21.link/n4950