Doc. no. | P1789R0 |
Date: | 2019-06-17 |
Project: | Programming Language C++ |
Audience: | Library Evolution Working Group |
Reply to: | Alisdair Meredith <ameredith1@bloomberg.net> |
Original version of the paper for the 2019 pre-Cologne mailing.
Expansion statements add compile-time iteration to C++20, but there are no compile-time sequences of integer that support this feature, for inuitive for loop indexing from the runtime world. There is an easy fix with a simple enhancement the library template integer_sequence to support the structured binding API.
Expansion statements are a new language feature for C++20, approved by EWG in Kona, and due to land in Cologne, 2019. See P1306R1 Expansion statement for details. They allow for iteration over parameter packs. However, my very first attempt at an example of using an expansion statement came up short, due to the omission of a tiny piece of supporting library infrastructure.
It is realtively straightforward to implement tuple::swap using a fold expression over the comma operator, but also somewhat of a hack. This is the kind of code we would like to be able to write more cleanly using an expansion statement.
template <class... TYPES> constexpr void tuple<TYPES...>::swap(tuple& other) noexcept((is_nothrow_swappable_v<TYPES> and ...)) { auto impl = [&, this]<size_t...INDEX>(index_sequence<INDEX...>) { ((void)swap(get<INDEX>(*this), get<INDEX>(other)), ...); }; impl(index_sequence_for<TYPES...>{}); }
Note the intenral use of a lambda expression, purely to get at the parameter pack to fold. Also note that we must cast the call to swap to void in case users provide an ADL-discoverable swap function that returns a user defined type, that in turn provides an overload for the comma operator. Finally, I took the liberty of not replicating the exception specification on the lambda expression, but is that relying too heavily on compilers to optimize away the (unneeded) catch-and-abort logic?
We can eliminate the fold expression and worrying about the crazy corner cases in ADL-swap like so:
template <class... TYPES> constexpr void tuple<TYPES...>::swap(tuple& other) noexcept((is_nothrow_swappable_v<TYPES> and ...)) { auto impl = [&, this]<size_t...INDEX>(index_sequence<INDEX...>) { for...(size_t N : INDEX...) { swap(get<N>(*this), get<N>(other)); } }; impl(index_sequence_for<TYPES...>{}); }
However, there is no easy way to eliminate the lamba expression, as we cannot iterate over an integer_sequence using just the facilities provided in P1306R1.
We propose that the simplest way to resolve the concerns is to add the missing pieces that would enable use of integer_sequence in a structured binding. That is sufficient to support use in expansion statements, and is general enough to be a feature in its own right. With such support, the tuple::swap example simplifies to:
template <class... TYPES> constexpr void tuple<TYPES...>::swap(tuple& other) noexcept((is_nothrow_swappable_v<TYPES> and ...)) { for...(size_t N : index_sequence_for<TYPES...>{}) { swap(get<N>(*this), get<N>(other)); } }
integer_sequence is missing three things in order to support use in structured bindings:
The first two bullets are fairly straightforward to implement. For the get function, we propose a single overload taking an integer_sequence by value, as it is an immutable empty type, and likewise returning its result by value.
template<class T, T... VALUES> struct tuple_size<integer_sequence<T, VALUES...>> : integral_constant<size_t, sizeof...(VALUES)> { }; template<size_t I, class T, T... VALUES> requires I < sizeof...(VALUES) struct tuple_element<I, integer_sequence<T, VALUES...>> { using type = T; }; template<size_t I, class T, T... VALUES> constexpr T get(integer_sequence<T, VALUES...>) noexcept { constexpr T index[]{VALUES...}; return index[I]; }
Make the following changes to the specified working paper:
20.2.1 Header <utility> synopsis [utility.syn]
- The header
contains some basic function and class templates that are used throughout the rest of the library. #include <initializer_list> // see 17.10.1 namespace std { // 20.2.2, swap ... // 20.3, Compile-time integer sequences template<class T, T...> struct integer_sequence; template<size_t... I> using index_sequence = integer_sequence<size_t, I...>; template<class T, T N> using make_integer_sequence = integer_sequence<T, see below>; template<size_t N> using make_index_sequence = make_integer_sequence<size_t, N>; template<class... T> using index_sequence_for = make_index_sequence<sizeof...(T)>; // forward declaration for structured binding support template<class T> struct tuple_size; template<size_t I, class T> struct tuple_element; // structured binding support for integer_sequence template<class T, T...> struct tuple_size<integer_sequence<T, T...>>; template<size_t I, class T, T...> struct tuple_element<I, integer_sequence<T, T...>>; template<size_t I, class T, T...> constexpr T get(integer_sequence<T, T...>) noexcept; // 20.4, class template pair template<class T1, class T2> struct pair; ... // 20.4.4, tuple-like access to pairtemplate<class T> struct tuple_size;template<size_t I, class T> struct tuple_element;template<class T1, class T2> struct tuple_size<pair<T1, T2>>; template<size_t I, class T1, class T2> struct tuple_element<I, pair<T1, T2>>; }20.3.4 Structured Binding Support [intseq.binding]
template<class T, T...> struct tuple_size<integer_sequence<T, T...>>: integral_constant<size_t, N> { }; template<size_t I, class T, T...> struct tuple_element<I, integer_sequence<T, T...>>::type
- Mandates: I < N is true.
- Value: The type T.
template<size_t I, class T, T...> constexpr T get(integer_sequence<T, T...>) noexcept;- Mandates: I < N is true.
- Returns: The Ith member of the parameter pack T....
Thanks to Vittorio Romeo and Daveed Vandevoorde for their insights into the contents of this paper.