Note: A change is required to the core language.
1. Introduction
While it is currently possible to return an unmovable-and-uncopyable 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
:
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 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
member function is written:
template < typename ... Params > T & emplace ( Params && ... args ) { . . . :: new ( buffer ) T ( forward < Params > ( args )... ); . . . }
or:
template < typename ... Params > T & emplace ( Params && ... args ) { . . . std :: construct_at < T > ( buffer , forward < Params > ( args )... ); . . . }
The compiler cannot find a constructor for
which accepts a sole argument
of type
, and so it invokes the conversion operator, meaning we
effectively have the constructor of
invoked as follows:
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 class to
the standard library called
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 <functional>// invoke #include <tuple>// apply, tuple #include <type_traits>// invoke_result, is_same, remove_reference, true_type #include <utility>// move namespace std { template < typename F_ref , typename ... Params_refs > class elide { public : typedef std :: true_type tag_elide ; private : using R = invoke_result_t < F_ref , Params_refs ... > ; static_assert ( is_same_v < R , remove_reference_t < R > > ); // F must return by value 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 public : template < typename F , typename ... Params > constexpr explicit elide ( F && arg , Params && ... args ) noexcept // see explicit deduction guide : f ( std :: move ( arg )), args_tuple ( static_cast < Params &&> ( args )... ) {} constexpr operator R ( void ) noexcept ( noexcept ( apply ( static_cast < F_ref > ( f ), std :: move ( args_tuple )))) { return apply ( static_cast < F_ref > ( f ), std :: move ( args_tuple ) ); } /* -------- Delete all miranda methods -------- */ elide ( void ) = delete ; elide ( elide const & ) = delete ; elide ( elide && ) = delete ; elide & operator = ( elide const & ) = delete ; elide & operator = ( elide && ) = delete ; elide const volatile * operator & ( void ) const volatile = delete ; template < typename U > void operator ,( U && ) = delete ; /* -------------------------------------------- */ }; template < typename F , typename ... Params > elide ( F && , Params && ...) -> elide < F && , Params && ... > ; // explicit deduction guide template < typename T > concept has_tag_elide_true = T :: tag_elide :: value ; }
Thoroughly tested on GodBolt: https://godbolt.org/z/xcfo1z39K
You can comment out Line #85 in the godbolt to see the effect of the core language change.
4. Design considerations
4.1. template constructor
The above implementation of
will not work in a situation where a class has a constructor which accepts a specialisation of the template class
as its sole argument, such as the following
:
class AwkwardClass { std :: mutex m ; // cannot move, cannot copy public : 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 ) ); }
The above program will print out:
-- In constructor for AwkwardClass, type of T = std::elide< AwkwardClass, AwkwardClass (&)(int), int&& > --
The problem here is that the constructor of
has been
instantiated with the template parameter type
set to a
specialisation of
, when really we wanted
to be set to
. 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
as follows:
template < typename ... Params > requires ( ! ( ( 1u == sizeof ...( Params )) && ( std :: has_tag_elide_true < Params > || ...) )) AwkwardClass ( Params && ... arg ) { ( std :: cout << "In constructor for AwkwardClass, type of T = " << typeid ( Params ). 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
from having a specialisation of
as its sole parameter.
This constraint is achieved by detecting the presence of a
called
for which
evaluates to true
.
The programmer can write their own custom class to use instead of
, and in order to take advantage of the core language
feature which prevents the instantiation of constructors, the programmer
must give their custom class an accessible
as follows:
typedef std :: true_type tag_elide ;
Furthermore, if the programmer so wishes, they can derive their own
class from
and change the
in order to disable
the failure of template instantiation:
template < typename F_ref , typename ... Params_refs > class MyElide : public std :: elide < F_ref , Params_refs ... > { public : typedef std :: false_type tag_elide ; . . . };
5. Alternatives
5.1. emplace_invoke
An alternative would be to add a method called
to
classes like
and
, as follows:
template < typename F , typename ... Params > T & emplace_invoke ( F && f , Params && ... args ) { . . . :: new ( buffer ) T ( std :: forward < F > ( f )( std :: forward < Params > ( args )... ) ); . . . }
The benefit of
is that it doesn’t require a change to
the core language. The benefit of
is that pre-existing
header files which implement or use the
method can be used
with
without requiring an alteration to the header file.
(For example, there would be no need to alter the Boost header
file for
in order to use it with
).
6. Proposed wording
The proposed wording is relative to [N4950].
In subclause 13.10.3.1.11 [temp.deduct.general], append a paragraph under the heading "Type deduction can fail for the following reasons:"
11 -- Attempting to instantiate a constructor that has exactly one parameter, and the sole parameter is a class type which contains an accessible typedef called 'tag_elide' whose 'value' evaluates to true, for example: typedef std::true_type tag_elide;
7. Impact on the standard
This proposal is a library extension combined with a change to the core language. The change to the core language is a paragraph to be added to 13.10.3.1.11 [temp.deduct.general]. The addition has no effect on any other part of the standard.
8. Impact on existing code
No existing code becomes ill-formed. The behaviour of all existing code is unaffected.
9. Revision history
R1 => R2
-
New section entitled 'Alternatives' to suggest that
andstd :: optional
could be given anstd :: variant
method.emplace_invoke -
can be used in a constant-evaluated context.std :: elide -
is given as an alternative to placementstd :: construct_at
(note that the former can be used in a constant-evaluated context).new -
The example of
is removed becausestd :: any
must be copy-constructible.T -
The example of
is added.boost :: static_vector -
The proposed wording for the standard clarifies that the accessible typedef is called
tag_elide -
Correction of typo,
->is_elider has_tag_elide_true -
Correction of typo,
->typename T typename ... Params
R0 => R1
-
The class
is not defined asstd :: elide final -
Template instantiation only fails if the constructor has exactly one parameter.
-
Template instantiation fails based upon the presence of a
tag whosetypedef
evaluates tovalue true
.
10. Acknowledgements
For their feedback and contributions at the BSI (British Standards Institution) C++ committee meetings:
Gašper Ažman, Oliver Rosten, Lénárd Szolnoki, Matthew Taylor
For their feedback and contributions on the mailing list std-proposals@lists.isocpp.org:
Jens Mauer, Jason McKesson, Ville Voutilainen, Jonathan Wakely
And for their insightful blogs:
Andrzej Krzemieński, Arthur O’Dwyer