New Proposal,

This version:
Latest version:
TPK Healy <healytpk@vir7ja7code7.com> (Remove all sevens from email address)
SG17, SG18
ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21


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

namespace std {
  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;  // 'f' is always an Rvalue reference
      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)... ) {}
      operator R(void) noexcept(noexcept(apply(static_cast<F_ref>(f),move(args_tuple))))
          return apply( static_cast<F_ref>(f), move(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 a class 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
    template<typename T>
    AwkwardClass(T &&arg)
        cout << "In constructor for AwkwardClass, \n"
                "type of T = " << typeid(T).name() << endl;

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

int main(int const argc, char **const argv)
    std::optional<AwkwardClass> var;
    var.emplace( std::elide(ReturnAwkwardClass, -1) );

This program will print out:

In constructor for AwkwardClass,
type of T = std::elide_t< AwkwardClass, AwkwardClass (&)(int), int&& >

The problem here is that the constructor of AwkwardClass has been instantiated with the template parameter type T set to a specialisation of std::elide_t, when really we wanted T to be set to int. We want the following output:

In constructor for AwkwardClass,
type of T = int

A workaround here is to apply a constraint to the constructor of AwkwardClass as follows:

template<typename T>requires (!is_specialization_v< std::remove_cvref_t<T>, std::elide_t >)AwkwardClass(T &&arg){    cout << "In constructor for AwkwardClass, \n"            "type of T = " << typeid(T).name() << 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 [temp.deduct.general], append a paragraph:

11 -- Attempting to instantiate a constructor in which a parameter
      has a type of 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 [temp.deduct.general]. The text addition is 17 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 existing code is unaffected by this addition to the standard library.


Normative References

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