1. Motivation
1.1. Allowing user-defined tuples, or "true" motivation :)
The section addresses the LEWG feedback. While we always had in mind that with 
So, let’s go to the problem statement. Today the C++ standard defines tuple-like types as only 5 types from 
- 
     std :: tuple 
- 
     std :: pair 
- 
     std :: array 
- 
     std :: complex 
- 
     std :: ranges :: subrange 
That sounds like a huge limitation for a generic library because, in principle, user-defined types could be treated like tuples. 
Furthermore, there is already partial support for user-defined tuples in the language. For example the structured binding language feature has special rules for finding a 
Unfortunately, rules are different in different places in the standard today. For example, has-tuple-element exposition-only concept for 
[P2165R4] added constraints for existing API (like 
Since the proposed 
For the following (simplified) code snippet:
namespace user { template < typename T , typename U > struct my_tuple_like { public : my_tuple_like ( T tt , U uu ) : t ( tt ), u ( uu ) {} private : T t ; U u ; template < std :: size_t I > friend auto get ( my_tuple_like < T , U > t_like ) { static_assert ( I == 0 || I == 1 ); if constexpr ( I == 0 ) return t_like . t ; else if ( I == 1 ) return t_like . u ; } }; } // namespace user namespace std { template < typename T , typename U > struct tuple_size < user :: my_tuple_like < T , U >> : std :: integral_constant < std :: size_t , 2 > {}; template < typename T , typename U > struct tuple_element < 0 , user :: my_tuple_like < T , U >> { using type = T ; }; template < typename T , typename U > struct tuple_element < 1 , user :: my_tuple_like < T , U >> { using type = U ; }; } // namespace std 
please see the Before-After table
| Before | After | 
|---|---|
| 
 | 
 | 
Of course, 
1.2. The original motivating use case
Having 
Let’s consider the following example:
std :: vector < std :: tuple < int , int >> v {{ 3 , 1 },{ 2 , 4 },{ 1 , 7 }}; std :: ranges :: sort ( v , []( auto x , auto y ) { // key-based sorting return std :: get < 0 > ( x ) < std :: get < 0 > ( y ); }); 
As we can see, users should spell some extra syntax out to achieve the necessary goal, comparing to what is described in § 1.2.2 The desired approach. The example above can be considered simplified; in real practice users might also need to think of e.g. adding references to lambda parameters to avoid copying.
The code above can be rewritten with structured binding:
std :: vector < std :: tuple < int , int >> v {{ 3 , 1 },{ 2 , 4 },{ 1 , 7 }}; std :: ranges :: sort ( v , []( auto x , auto y ) { // key-based sorting auto [ key1 , value1 ] = x ; auto [ key2 , value2 ] = y ; return key1 < key2 ; }); 
Though one could say that it makes code simpler or at least more readable, on the other hand, its syntax forces the programmer to give names to otherwise unneeded variables, which is often considered a bad practice.
With [P2169R3] the situation with unused variables for structured binding becomes better but still might require the user to write a quite amount of underscores depending on the use case:
std :: vector < std :: tuple < int , int , int , int >> v {{ 3 , 1 , 1 , 1 },{ 2 , 4 , 4 , 4 },{ 1 , 7 , 7 , 7 }}; std :: ranges :: sort ( v , []( auto x , auto y ) { // key-based sorting auto [ key1 , _ , _ , _ ] = x ; auto [ key2 , _ , _ , _ ] = y ; return key1 < key2 ; }); 
1.2.1. Projections-based alternative
Projections provide another option to achieve the same behavior:
std :: ranges :: sort ( v , std :: less {}, []( auto x ) { // key-based sorting return std :: get < 0 > ( x ); }); 
A variant that properly handles references would use a generic lambda:
[]( auto && x ) -> auto && { // key-based sorting return std :: get < 0 > ( std :: forward < decltype ( x ) > ( x )); } 
While this code achieves the desired result, it requires more syntactic boilerplate (lambda, forwarding etc.) than the useful code.
1.2.2. The desired approach
The nicest way to get what we want would be:
// The code that does not work because std::get is not fully instantiated std :: ranges :: sort ( v , std :: less {}, std :: get < 0 > ); 
But it doesn’t work because 
1.2.3. Why not std :: ranges :: views :: elements 
   The necessary result cannot be achieved with 
| std::ranges::views::elements | Desired behavior | 
|---|---|
| 
 | 
 | 
1.3. Usefulness with zip_view 
   With 
1.4. Radix sort use case
Counting-based sorts, and Radix Sort in particular, provide another motivating use case.
Today it is not possible to have a C++ standard conformant implementation that uses
Radix Sort algorithm underneath because the complexity of 
However, the industry needs Radix Sort for performance reasons. Implementations of C++ standard
parallel algorithms, such as oneAPI Data Parallel C++ Library (oneDPL) and CUDA Thrust, use Radix Sort
conditionally under the hood of 
That makes the proposed API applicable wider than just with the C++ standard library use cases.
2. Proposed API
We propose the following API:
inline namespace /* unspecified */ { template < size_t I > inline constexpr /* unspecified */ get_element = /* unspecified */ ; } inline constexpr auto get_key = get_element < 0 > ; inline constexpr auto get_value = get_element < 1 > ; 
With that API the motivating use case code with the desired behavior would be:
std :: vector < std :: tuple < int , int >> v {{ 3 , 1 },{ 2 , 4 },{ 1 , 7 }}; std :: ranges :: sort ( v , std :: less {}, std :: get_element < 0 > ); 
or even
std :: vector < std :: tuple < int , int >> v {{ 3 , 1 },{ 2 , 4 },{ 1 , 7 }}; std :: ranges :: sort ( v , std :: less {}, std :: get_key ); 
Let’s look at comparison tables (a.k.a. Tony Tables):
Comparison of proposed API with comparator-based version
| Before | After | 
|---|---|
| 
 | 
 | 
Comparison of proposed API with projections-based version
| Before | After | 
|---|---|
| 
 | 
 | 
2.1. Possible implementation
namespace std { namespace __detail { template < std :: size_t _Ip > struct __get_element_fn { template < typename _TupleLike > auto operator ()( _TupleLike && __tuple_like ) const -> decltype ( get < _Ip > ( std :: forward < _TupleLike > ( __tuple_like ))) { return get < _Ip > ( std :: forward < _TupleLike > ( __tuple_like )); } }; } // namespace __detail inline namespace __get_element_namespace { template < std :: size_t _Ip > inline constexpr __detail :: __get_element_fn < _Ip > get_element ; } // inline namespace __get_element_namespace inline constexpr auto get_key = get_element < 0 > ; inline constexpr auto get_value = get_element < 1 > ; } // namespace std 
2.2. tuple-like concept
With the proposed 
2.2.1. tuple-like concept generalization with get_element 
   With 
// necessary to check if std::tuple_size_v is well-formed before using it template < typename T > concept /*has-tuple-size*/ = // exposition only requires { typename std :: tuple_size < T >:: type ; }; template < class T , std :: size_t N > concept /*can-get-tuple-element*/ = // exposition only /*has-tuple-size*/ < T > && requires ( T t ) { requires N < std :: tuple_size_v < T > ; typename std :: tuple_element_t < N , T > ; { std :: get_element < N > ( t ) } -> std :: convertible_to < const std :: tuple_element_t < N , T >&> ; }; 
Then the tuple-like concept can use can-get-tuple-element and do something like:
template < typename T > concept /*tuple-like*/ = ! std :: is_reference_v < T > && /*has-tuple-size*/ < T > && [] < std :: size_t ... I > ( std :: index_sequence < I ... > ) { return (... && /*can-get-tuple-element*/ < T , I > ); } ( std :: make_index_sequence < std :: tuple_size_v < T >> {}); 
3. Design considerations
Alternative name for the proposed API could be 
Potentially 
As 
3.1. What could be done to use std :: ranges :: get 
   In all major standard library implementations (GCC, LLVM, Microsoft) the 
However, library implementors could move the current 
Please see the example that explains the idea and shows how it might look like. A full implementation with examples is available here.
namespace std { namespace ranges { // Necessary to make namespace __detail being considered by ADL // for get<0>(std::ranges::subrange<something>{}) without moving // the subrange itself to another namespace namespace __detail { struct adl_hook {}; } // thanks to the empty-base optimization, inheriting adl_hook does not break ABI template < class T > class subrange : __detail :: adl_hook { public : T whatever ; }; namespace __detail { template < std :: size_t , class T > auto get ( subrange < T > x ) { return x . whatever ; } } // namespace __detail } // namespace ranges using std :: ranges :: __detail :: get ; } // namespace std namespace std { namespace ranges { namespace __detail { // Introduce Args... to cover the case of calling get with explicit template arguments template < std :: size_t _Ip , typename ... Args > struct __get_fn { // No more than std::tuple_size_v template arguments should be allowed template < typename _TupleLike > requires ( sizeof ...( Args ) <= std :: tuple_size_v < std :: remove_cvref_t < _TupleLike >> && __are_tuple_elements_convertible_to_args < std :: remove_cvref_t < _TupleLike > , Args ... >:: value ) decltype ( auto ) operator ()( _TupleLike && __tuple_like ) const { return get < _Ip > ( std :: forward < _TupleLike > ( __tuple_like )); } }; } // namespace __detail inline namespace __get_fn_namespace { template < std :: size_t _Ip , typename ... Args > inline constexpr __detail :: __get_fn < _Ip , Args ... > get ; } // inline namespace __get_fn_namespace } // namespace ranges } // namespace std 
With such an implementation, all important cases from our perspective continue working:
- 
     std :: ranges :: get < 0 > ( sub_r ) 
- 
     std :: get < 0 > ( sub_r ) 
- 
     get < 0 > ( sub_r ) 
- 
     std :: ranges :: get < 0 , some_arg > ( sub_r ) 
where 
The API breaking change appears when 
Definitely such a change would break the ABI for 
Since the 
4. Connections with other papers
4.1. Connection with [P2547R1]
[P2547R1] uses 
Moreover, at this time the authors of [P2547R1] don’t see how to introduce customizable functions
with the same names (e.g. 
4.2. Connection with [P2141R1]
[P2141R1]'s main goal is allow aggregates being interpreted as Tuple-Like. At the same time, it touches
the tuple-like concept making it as generic as for the types structured binding can work with. It also adds
a yet another 
With [P2141R1] being adopted 
Independently of [P2141R1] 
[P2141R1] also gives another way to generalize the tuple-like concept (via structured binding).
5. Further work
- 
     Substitute std::get_element for std::get in formal wording of the APIs with tuple-like
- 
     Broader implementation experience 
6. Formal wording
Below, substitute the � character with a number the editor finds appropriate for the table, paragraph, section or sub-section.
6.1. Modify Concept tuple-like [tuple.like]
template < typename T > concept has - tuple - size = // exposition only requires { typename tuple_size < T >:: type ; }; template < class T , size_t N > concept can - get - tuple - element = // exposition only has - tuple - size < T > && requires ( T t ) { requires N < std :: tuple_size_v < T > ; typename std :: tuple_element_t < N , T > ; { std :: get_element < N > ( t ) } -> std :: convertible_to < const std :: tuple_element_t < N , T >&> ; }; template < typename T > concept tuple - like = see - below ! is_reference_v < T > && has - tuple - size < T > && [] < size_t ... I > ( index_sequence < I ... > ) { return (... && ranges :: can - get - tuple - element < T , I > ); } ( make_index_sequence < tuple_size_v < T >> {}); A typemodels and satisfies the exposition-only concept tuple-like ifT is a specialization ofremove_cvref_t < T > ,array ,complex ,pair , ortuple .ranges :: subrange 
6.2. Modify Header < tuple > 
   [...]// [tuple.helper], tuple helper classes template < class T > constexpr size_t tuple_size_v = tuple_size < T >:: value ; inline namespace /* unspecified */ { template < size_t I > inline constexpr /* unspecified */ get_element = /* unspecified */ ; } inline constexpr auto get_key = get_element < 0 > ; inline constexpr auto get_value = get_element < 1 > ; 
6.3. Add the following sections into [tuple]
[...]
� Element access [tuple.elem]
� Customization Point Objects [tuple.cust] �[tuple.cust.get_elem]get_element 
6.4. Add the following wording into [tuple.cust.get_elem]
1. The namedenotes a customization point object ([customization.point.object]). The expressionget_element whereget_element < I > ( E ) isI for a subexpressionsize_t is expression-equivalent to:E 
, ifget < I > ( E ) has class or enumeration type andE is a well-formed expression when treated as an unevaluated operand, where the meaning ofget < I > ( E ) is established as-if by performing argument-dependent lookup only ([basic.lookup.argdep]).get 
Otherwise,
is ill-formed.get_element < I > ( E ) 
6.5. Add feature test macro to the end of [version.syn]
[...]#define __cpp_lib_element_access_customization_point 20����L // also in <tuple> , <utility> , <array> , <ranges> [...]
6.6. Modify tuple 
   template < tuple - like UTuple > 
constexpr explicit ( see below ) tuple ( UTuple && u ); Let
be the packI .0 , 1 , …, ( sizeof ...( Types ) - 1 ) Constraints:
([range.utility.helpers]) isdifferent - from < UTuple , tuple > true,
is not a specialization ofremove_cvref_t < UTuple > ,ranges :: subrange 
equalssizeof ...( Types ) ,tuple_size_v < remove_cvref_t < UTuple >> 
( is_constructible_v < Types , decltype ( get _element is< I > ( std :: forward < UTuple > ( u ))) > && ...) true, and
either
is not 1, or (whensizeof ...( Types ) expands toTypes ... )T andis_convertible_v < UTuple , T > are bothis_constructible_v < T , UTuple > false.Effects: For all i, initializes the ith element of
with* this get _element < i > ( std :: forward < UTuple > ( u )). Remarks: The expression inside explicit is equivalent to:
! ( is_convertible_v < decltype ( get _element The constructor is defined as deleted if< I > ( std :: forward < UTuple > ( u ))), Types > && ...) ( reference_constructs_from_temporary_v < Types , decltype ( get _element is< I > ( std :: forward < UTuple > ( u ))) > || ...) true.
6.7. Modify tuple 
   template < tuple - like UTuple > 
constexpr tuple & operator = ( UTuple && u ); Constraints:
([range.utility.helpers]) isdifferent - from < UTuple , tuple > true,
is not a specialization ofremove_cvref_t < UTuple > ,ranges :: subrange 
equalssizeof ...( Types ) , and,tuple_size_v < remove_cvref_t < UTuple >> 
is_assignable_v < Ti & , decltype ( get _element is< i > ( std :: forward < UTuple > ( u ))) > truefor all i.Effects: For all i, assigns
get _element to< i > ( std :: forward < UTuple > ( u )) get _element .< i > ( * this ) Returns:
.* this 
template < tuple - like UTuple > 
constexpr const tuple & operator = ( UTuple && u ) const ; Constraints:
([range.utility.helpers]) isdifferent - from < UTuple , tuple > true,
is not a specialization ofremove_cvref_t < UTuple > ,ranges :: subrange 
equalssizeof ...( Types ) , and,tuple_size_v < remove_cvref_t < UTuple >> 
is_assignable_v < const Ti & , decltype ( get _element is< i > ( std :: forward < UTuple > ( u ))) > truefor all i.Effects: For all i, assigns
get _element to< i > ( std :: forward < UTuple > ( u )) get _element .< i > ( * this ) Returns:
.* this 
6.8. Modify tuple_cat 
   template < tuple - like ... Tuples > 
constexpr tuple < CTypes ... > tuple_cat ( Tuples && ... tpls ); Let n be
. For every integersizeof ...( Tuples ) :0 <= i < n 
Let
be the ith type inTi .Tuples 
Let
beUi .remove_cvref_t < Ti > 
Let
be the ith element in the function parameter packtpi .tpls 
Let
beSi .tuple_size_v < Ui > 
Let
beEki .tuple_element_t < k , Ui > 
Let
beeki get _element .< k > ( std :: forward < Ti > ( tpi )) 
Let
be a pack of the typesElemsi .E0i ,..., ESi −1 i 
Let
be a pack of the expressionselemsi ,...,e0i .eSi −1 i The types in
are equal to the ordered sequence of the expanded packs of typesCTypes . LetElems0 ..., Elems1 ..., ..., Elemsn −1. .. be the ordered sequence of the expanded packs of expressionscelems .elems0 ..., ..., elemsn −1. .. Mandates:
is( is_constructible_v < CTypes , decltype ( celems ) > && ...) true.Returns:
tuple < CTypes ... > ( celems ...) 
6.9. Modify apply 
   template < class F , tuple - like Tuple > 
constexpr decltype ( auto ) apply ( F && f , Tuple && t ) noexcept ( see below ); Effects: Given the exposition-only function template:
namespace std { template < class F , tuple - like Tuple , size_t ... I > constexpr decltype ( auto ) apply - impl ( F && f , Tuple && t , index_sequence < I ... > ) { // exposition only return INVOKE ( std :: forward < F > ( f ) , get _element < I > ( std :: forward < Tuple > ( t ))...); // see [func.require] } } Equivalent to:
return apply - impl ( std :: forward ( f ), std :: forward ( t ), make_index_sequence < tuple_size_v < remove_reference_t < Tuple >>> {}); Remarks: Let
be the packI . The exception specification is equivalent to:0 , 1 , ..., ( tuple_size_v < remove_reference_t < Tuple >> - 1 ) noexcept ( invoke ( std :: forward < F > ( f ), get _element < I > ( std :: forward < Tuple > ( t ))...)) 
template < class T , tuple - like Tuple > constexpr T make_from_tuple ( Tuple && t ); Mandates: If
is 1, thentuple_size_v < remove_reference_t < Tuple >> 
reference_constructs_from_temporary_vT , decltype ( get _element is< 0 > ( declval < Tuple > ())) > false.Effects: Given the exposition-only function template:
namespace std { template < class T , tuple - like Tuple , size_t ... I > requires is_constructible_v < T , decltype ( get _element < I > ( declval < Tuple > ()))... > constexpr T make - from - tuple - impl ( Tuple && t , index_sequence < I ... > ) { // exposition only return T ( get _element < I > ( std :: forward < Tuple > ( t ))...); } } 
6.10. Modify relation operators in [tuple.rel]
template < class ... TTypes , class ... UTypes > constexpr bool operator == ( const tuple < TTypes ... >& t , const tuple < UTypes ... >& u ); template < class ... TTypes , tuple - like UTuple > constexpr bool operator == ( const tuple < TTypes ... >& t , const UTuple & u ); For the first overload let
beUTuple .tuple < UTypes ... > Constraints: For all
, wherei ,0 <= i < sizeof ...( TTypes ) get < i > ( t ) == get _element is a valid expression and< i > ( u ) decltype ( get < i > ( t ) == get _element models boolean-testable.< i > ( u )) equalssizeof ...( TTypes ) .tuple_size_v < UTuple > Returns:
trueifget < i > ( t ) == get _element for all< i > ( u ) , otherwisei false.[Note 1: If
equals zero, returnssizeof ...( TTypes ) true. — end note]Remarks:
The elementary comparisons are performed in order from the zeroth index upwards. No comparisons or element accesses are performed after the first equality comparison that evaluates to
false.
The second overload is to be found via argument-dependent lookup ([basic.lookup.argdep]) only.
template < class ... TTypes , class ... UTypes > constexpr common_comparison_category_t < synth - three - way - result < TTypes , UTypes > ... > operator <=> ( const tuple < TTypes ... >& t , const tuple < UTypes ... >& u ); template < class ... TTypes , tuple - like UTuple > constexpr common_comparison_category_t < synth - three - way - result < TTypes , Elems > ... > operator <=> ( const tuple < TTypes ... >& t , const UTuple & u ); For the second overload,
denotes the pack of typesElems .tuple_element_t < 0 , UTuple > , tuple_element_t < 1 , UTuple > , …, tuple_element_t < tuple_size_v < UTuple > - 1 , UTuple > Effects: Performs a lexicographical comparison between
andt . Ifu equals zero, returnssizeof ...( TTypes ) .strong_ordering :: equal Otherwise, equivalent to:
if ( auto c = synth - three - way ( get < 0 > ( t ), get _element < 0 > ( u )); c != 0 ) return c ; return ttail <=> utail ; where
for somertail is a tuple containing all but the first element ofr .r Remarks: The second overload is to be found via argument-dependent lookup ([basic.lookup.argdep]) only.
6.11. Modify [range.elements.iterator]
The member typedef-name
is defined if and only if Base modelsiterator_category . In that case,forward_range is defined as follows: Letiterator_category denote the typeC iterator_traits < iterator_t < Base >> :: iterator_category . 
If
std :: get _element is an rvalue,< N > ( * current_ ) denotesiterator_category .input_iterator_tag 
Otherwise, if C models
,derived_from < random_access_iterator_tag > denotesiterator_category random_access_iterator_tag . 
Otherwise,
denotesiterator_category .C 
static constexpr decltype ( auto ) get - element ( const iterator_t < Base >& i ); Effects: Equivalent to:
if constexpr ( is_reference_v < range_reference_t < Base >> ) { return std :: get _element < N > ( * i ); } else { using E = remove_cv_t < tuple_element_t < N , range_reference_t < Base >>> ; return static_cast < E > ( std :: get _element < N > ( * i )); } 
7. Revision history
7.1. R1 => R2
- 
     Add extra motivation to allow user-defined tuples in the standard 
- 
     Propose changes to tuple-like concept with wording 
7.2. R0 => R1
- 
     Address the "structured binding unused variables" questions with [P2169R3] 
- 
     Add ruminations about possible relaxation of the tuple-like concept 
- 
     Apply an approach to minimize ABI and API breaking for the std :: ranges :: get 
- 
     Add wording and a feature test macro for the std :: get_element 
8. Polls
8.1. SG9 polls, Issaquah 2023
POLL: The solution proposed in the paper "P2769: 
| SF | F | N | A | SA | 
|---|---|---|---|---|
| 1 | 2 | 1 | 2 | 1 | 
POLL: The solution proposed in the paper "P2769: 
| SF | F | N | A | SA | 
|---|---|---|---|---|
| 2 | 4 | 0 | 1 | 0 | 
8.2. Library Evolution Telecon 2024-01-23
POLL: [P2769R1] (
| SF | F | N | A | SA | 
|---|---|---|---|---|
| 3 | 3 | 4 | 2 | 0 | 
POLL: LEWG should spend more time on [P2769R1] (
| SF | F | N | A | SA | 
|---|---|---|---|---|
| 5 | 4 | 0 | 2 | 0 | 
9. Acknowledgements
- 
     Thanks to Casey Carter for providing adl_hook 
- 
     Thanks to Corentin Jabot for providing the input for § 4.1 Connection with [P2547R1] and for the initial tuple-like concept relaxing idea 
- 
     Thanks to Benjamin Brock for paper review and design discussions.