1. Introduction
The tuple protocol has been introduced in C++11.
If
is a tuple-like type:
-
It can be destructured into
elements;std :: tuple_size_v < T > -
Its
-th element has typeI
and can be extracted through thestd :: tuple_element_t < I , T >
function template.std :: get
Since C++17, the tuple protocol interacts with the core language, which allows structured bindings to expressions of tuple-like types.
The standard already mandates the following types to be tuple-like:
-
(since C++20),std :: ranges :: subranges -
(since C++11),std :: tuple -
(since C++11),std :: pair -
and, most relevant to this paper,
(since C++11).std :: array
In this paper, I propose the standard should make C-style arrays of known
bound,
, tuple-like too.
The implementation of the tuple-like protocol (i.e.
,
and
) I propose is designed after the existing
one for
.
2. Motivation
As far as their tuple-like properties are concerned,
and
are equivalent.
Both have a fixed number of elements,
, which is known at compile-time; each
element being of type
and accessible by a compile-time index.
Implementing the tuple-like protocol for C-style arrays would make them eligible to be passed as parameters to:
(since C++17),std :: apply
(since C++17)std :: make_from_tuple -
and,
(since C++11)std :: tuple_cat Prior to [P2165R3], the choice whether to support tuple-like types besides
instd :: tuple
was up to the implementers.std :: tuple_cat
In sections § 2.1 Automatic size deduction and § 2.2 Interacting with C APIs I outline some use cases where
may be preferable over
.
In such cases, being able to call the above functions without the need for a
temporary
would be beneficial.
2.1. Automatic size deduction
Unlike
, compilers are able to deduce the size (only) of a C-style
array from the number of elements in its initializer list:
int c_arr [] = { 0 , 1 , 2 }; static_assert ( sizeof ( c_arr ) / sizeof ( c_arr [ 0 ]) == 3 , "" );
This limitation of
was noted by Alisdair Meredith in [N1479] itself and lead Zhihao Yuan to float the idea of extending the implementation
of the tuple protocol to C-style arrays in [ARRAY-AS-A-TUPLE].
CTAD (since C++17) for
mitigates this problem but doesn’t allow a
user to specify the element type and deduce the size only.
Function template
(since C++20) does but poses constraints on
the element type (namely, it has be copy- or move-constructible and non-array).
2.2. Interacting with C APIs
C (or C-like) API may force users to deal with C-style arrays:
// File: geometry_c_api.h #define GEOMETRY_STATUS_OK 0 struct ReferenceFrame ; int get_origin ( struct ReferenceFrame * frame , double ( * pt )[ 3 ]);
If this paper gets accepted, client code might look like this:
class Point { public : explicit Point ( double x , double y , double z ); // ... }; std :: optional < Point > get_origin ( ReferenceFrame & frame ) { double pt [ 3 ] { }; if ( get_origin ( & frame , & pt ) != GEOMETRY_STATUS_OK ) return { }; return std :: make_from_tuple < Point > ( pt ); }
2.3. Compile-time bound check
A useful side benefit of the tuple protocol is the bound check performed by
function template
:
int c_arr [ 42 ]{}; // This would not not compile because index 42 is out of bounds //std::get<42>(c_arr) = 42; // Not OK: this is UB and compiles c_arr [ 42 ] = 42 ;
Making
tuple-like would help users prevent the above class of bugs
without any need for static analysis tools or sanitizers.
3. Impact on the standard
This proposal is a pure library extension.
It proposes changes to an existing header,
, but it does not require
changes to any standard classes or functions.
This proposal does not require changes in the core language. It does not produce changes in the core language either. Even though the tuple protocol interferes with the core language, which provides structured-binding support for tuple-like types, the standard already defines special rules for structured bindings to C-style arrays.
This proposal does not depend on any other library extension. In section § 4.1 Proposed implementation, I propose an implementation in standard C++11.
3.1. Interaction with other papers
With this proposal,
would satisfy exposition-only concept
introduced by Corentin Jabot with [P2165R3].
So,
s and
s would be constructible from and comparable
with C-style arrays:
int c_arr [] = { 0 , 1 }; //std::tuple<int, int, int> t = c_arr; // Error: different tuple size std :: pair < int , int > p = c_arr ; p == c_arr ; // Ok: evaluates to true p < c_arr ; // Ok: evaluates to false
int c_arr [] = { 0 , 1 , 2 }; std :: tuple < int , int , int > t = c_arr ; //std::pair<int, int> p = c_arr; // Error: different tuple size t == c_arr ; // Ok: evaluates to true t < c_arr ; // Ok: evaluates to false
4. Design decisions
4.1. Proposed implementation
In the following subsections, I outline my proposed implementation of the tuple
protocol for C-style arrays of known bound in standard C++11.
My implementation is designed after
's.
4.1.1. std :: tuple_size
For the
class template, I propose the following specializations:
namespace std { // (ts) template < typename T , size_t N > struct tuple_size < T [ N ] > : public integral_constant < size_t , N > { }; // (ts.c) template < typename T , size_t N > struct tuple_size < T const [ N ] > : public integral_constant < size_t , N > { }; // (ts.v) template < typename T , size_t N > struct tuple_size < T volatile [ N ] > : public integral_constant < size_t , N > { }; // (ts.cv) template < typename T , size_t N > struct tuple_size < T volatile const [ N ] > : public integral_constant < size_t , N > { }; }
Please note that specializations
,
and
are required because:
-
The standard already defines cv-qualified specializations for
;std :: tuple_size -
Applying cv-qualifiers to an array type applies the qualifiers to the element type and any array type whose elements are of cv-qualified type is considered to have the same cv-qualification [CPP-REF-ARRAY].
So, by not defining (say)
, the following code
using const_array_t = int const [ 42 ]; static_assert ( std :: tuple_size < array_t >:: value == 42 , "Size OK" );
would fail to compile because the template instanciation for
is ambiguous.
In fact both the following specializations would be viable candidates:
namespace std { // Already in the standard template < class T > struct tuple_size < const T > : public integral_constant < size_t , tuple_size < T >:: value > { }; // With T = int[42] // Proposed in this paper template < typename T , size_t N > struct tuple_size < T [ N ] > : public integral_constant < size_t , N > { }; // With T = int const, N = 42 }
The same holds for specializations
and
.
4.1.2. std :: tuple_element
For the
class template, I propose the following specializations:
namespace std { // (te) template < size_t Idx , typename T , size_t N > struct tuple_element < Idx , T [ N ] > { static_assert ( Idx < N , "Index out of bounds" ); using type = T ; }; // (te.c) template < size_t Idx , typename T , size_t N > struct tuple_element < Idx , T const [ N ] > { static_assert ( Idx < N , "Index out of bounds" ); using type = T const ; }; // (te.v) template < size_t Idx , typename T , size_t N > struct tuple_element < Idx , T volatile [ N ] > { static_assert ( Idx < N , "Index out of bounds" ); using type = T volatile ; }; // (te.cv) template < size_t Idx , typename T , size_t N > struct tuple_element < Idx , T volatile const [ N ] > { static_assert ( Idx < N , "Index out of bounds" ); using type = T volatile const ; }; }
The reason for introducing specializations
,
and
is the
same that lead to the introduction of specializations
,
and
in section § 4.1.1 std::tuple_size.
4.1.3. std :: get
For the
function templates, I propose:
namespace std { template < size_t Idx , typename T , size_t N > constexpr T & get ( T ( & arr )[ N ]) noexcept { static_assert ( Idx < N , "Index out of bounds" ); return arr [ Idx ]; } template < size_t Idx , typename T , size_t N > constexpr T && get ( T ( && arr )[ N ]) noexcept { static_assert ( Idx < N , "Index out of bounds" ); return move ( arr [ Idx ]); } }
4.2. Alternative implementation
Another possible implementation for the C-style array specializations of class
templates
and
in C++20 is the following
(courtesy of Arthur O’Dwyer):
namespace std { template < typename T , size_t N > requires ( is_same_v < T , remove_cv_t < T >> ) struct tuple_size < T [ N ] > : public integral_constant < size_t , N > {}; template < size_t Idx , typename T , size_t N > requires ( is_same_v < T , remove_cv_t < T >> ) struct tuple_element < Idx , T [ N ] > { static_assert ( Idx < N , "Index out of bounds" ); using type = T ; }; }
The
clause SFINAEs out the specializations of
and
for cv-qualified array types and prevents the
ambiguous-template-instantiation compilation error described in section § 4.1.1 std::tuple_size.
As for the
function templates, implementing them by means of the
clause is not feasible.
In fact, the following:
namespace std { template < size_t Idx , typename T , size_t N > requires ( Idx < N ) constexpr T & get ( T ( & arr )[ N ]) noexcept { return arr [ Idx ]; } template < size_t Idx , typename T , size_t N > requires ( Idx < N ) constexpr T && get ( T ( && arr )[ N ]) noexcept { return move ( arr [ Idx ]); } }
may lead to an inconsistent behavior with the existing implementation for
in unevaluated contexts:
std :: array < int , 42 > cpp_arr {}; using cpp_elem_ptr_t = decltype ( & std :: get < 42 > ( cpp_arr )); // cpp_elem_ptr_t is int* //int c_arr[42]{}; //using c_elem_ptr_t = decltype(&std::get<42>(c_arr)); // error: no matching function for call to 'get<42>(int [42])'
5. Technical specifications
In this section, I present the changes I propose to the standard. The wording is based on [N4910].
Modify section "Header
synopsis
":
// 22.4.6, tuple helper classes template < class T > struct tuple_size ; // not defined template < class T > struct tuple_size < const T > ; template < class ... Types > struct tuple_size < tuple < Types ... >> ;
template < class T , size_t N > struct tuple_size < T [ N ] > ; template < class T , size_t N > struct tuple_size < T const [ N ] > ; template < class T , size_t N > struct tuple_size < T volatile [ N ] > ; template < class T , size_t N > struct tuple_size < T volatile const [ N ] > ;
template < size_t I , class T > struct tuple_element ; // not defined template < size_t I , class T > struct tuple_element < I , const T > ; template < size_t I , class ... Types > struct tuple_element < I , tuple < Types ... >> ;
template < size_t I , class T , size_t N > struct tuple_element < I , T [ N ] > ; template < size_t I , class T , size_t N > struct tuple_element < I , T const [ N ] > ; template < size_t I , class T , size_t N > struct tuple_element < I , T volatile [ N ] > ; template < size_t I , class T , size_t N > struct tuple_element < I , T volatile const [ N ] > ;
template < size_t I , class T > using tuple_element_t = typename tuple_element < I , T >:: type ; // 22.4.7, element access template < size_t I , class ... Types > constexpr tuple_element_t < I , tuple < Types ... >>& get ( tuple < Types ... >& ) noexcept ; ... template < class T , class ... Types > constexpr const T && get ( const tuple < Types ... >&& t ) noexcept ;
template < size_t I , class T , size_t N > constexpr T & get ( T ( & arr )[ N ]) noexcept ; template < size_t I , class T , size_t N > constexpr T && get ( T ( && arr )[ N ]) noexcept ;
Modify section "Tuple helper classes
":
template < class T > struct tuple_size ;
1 All specializations of
meet the Cpp17UnaryTypeTrait requirements (21.3.2) with a base
characteristic of
for some
.
template < class ... Types > struct tuple_size < tuple < Types ... >> : public integral_constant < size_t , sizeof ...( Types ) > { };
template < class T , size_t N > struct tuple_size < T [ N ] > : public integral_constant < size_t , N > { }; template < class T , size_t N > struct tuple_size < T const [ N ] > : public integral_constant < size_t , N > { }; template < class T , size_t N > struct tuple_size < T volatile [ N ] > : public integral_constant < size_t , N > { }; template < class T , size_t N > struct tuple_size < T volatile const [ N ] > : public integral_constant < size_t , N > { };
2 Mandates:template < size_t I , class ... Types > struct tuple_element < I , tuple < Types ... >> { using type = TI ; };
I < sizeof ...( Types )
.3 Type:
TI
is the type of the I
-th element of Types
, where indexing is zero-based.
4 Mandates:template < size_t I , class T , size_t N > struct tuple_element < I , T [ N ] > { using type = T ; };
I < N
.
5 Mandates:template < size_t I , class T , size_t N > struct tuple_element < Idx , T const [ N ] > { using type = T const ; };
I < N
.
6 Mandates:template < size_t I , class T , size_t N > struct tuple_element < Idx , T volatile [ N ] > { using type = T volatile ; };
I < N
.
7 Mandates:template < size_t I , class T , size_t N > struct tuple_element < Idx , T volatile const [ N ] > { using type = T volatile const ; };
I < N
.
Append to section "Element access
":
9 Mandates:template < size_t I , class T , size_t N > constexpr T & get ( T ( & arr )[ N ]) noexcept ; template < size_t I , class T , size_t N > constexpr T && get ( T ( && arr )[ N ]) noexcept ;
I < N
.10 Returns: A reference to the
I
-th element of arr
, where indexing is zero-based.
6. Questions
-
Why are fixed-extent
s non tuple-like [P2116R0]?std :: span -
specializations forvolatile
andstd :: tuple_size
have been deprecated.std :: tuple_element -
Why?
-
Should I propose them for
?T [ N ]
-
7. Acknowledgements
I’d like to thank (sorted by
)
-
Arthur O’Dwyer,
-
Barry Revzin,
-
Giuseppe D’Angelo,
-
Jason McKesson,
-
Lénárd Szolnoki,
-
Nikolay Mihaylov,
-
Zhihao Yuan
for their valuable feedbacks which made this paper possible.