Document number: P0825R1
Date: 2018-2-10
Project: Programming Language C++, Library Evolution Working Group
Reply-to: Agustín Bergé agustinberge@gmail.com
A friendlier tuple get
0. History
Changes from P0825R0:
- Add proposed wording.
- Move alternative designs discussion to appendix.
1. Introduction
This paper proposes changing std::get overloads to behave gracefully in the presence of user defined get overloads.
2. Motivation
Consider the following example, presented on Cpplang at Slack:
template <typename... Ts>struct WeirdTuple : private std::tuple<Ts...> { using std::tuple<Ts...>::tuple;};template <std::size_t I, typename... Ts>auto get(WeirdTuple<Ts...>& t) { return I + 10; }int main() { WeirdTuple<int> wt(1); get<0>(wt); // changing this 0 to anything else will break // theoretically, that should still work - // we're obviously using the WeirdTuple overload of get<>(), // but the compiler fails when trying to compile every possible overload!} |
After the alluded change, the result is:
get<1>(wt); // error: static assertion failed: tuple index is in range // in instantiation of 'std::tuple_element<1, std::tuple<int>>' // required by substitution of // constexpr std::tuple_element_t<I, std::tuple<Ts...>>& // std::get(std::tuple<Ts...>&) // [with I = 1; Ts = {int}] |
The confusion arises from a disagreement between the programmer and the implementation on the "obviousness" of the intented target. A sufficiently advanced implementation might realize that no std::get overload would possibly be a better match than the WeirdTuple overload and thus skip substitution altogether, but it is not required to do so. During that substitution process the std::get overloads render the program ill-formed, effectively poisoning the overload set.
3. Discussion
LEWG discussion in Albuquerque favored a solution based on conditionally deleting std::get overloads for out-of-bounds calls (see Alternative Designs):
APPROVAL VOTE:
- Option #1: SFINAE-friendly 2
- Option #2: Conditionally Deleted 8
- Option #3: Deduced Return Type 4
Conditionally deleted get overloads solve the issue by deferring the effect of making the program ill-formed to the point in which the overload is actually used, rather than when forming the candidate overload set:
WeirdTuple<int> wt(1);get<0>(wt); // ok, returns 10get<1>(wt); // ok too, returns 11 |
Another consequence of this deferred effect is that it makes the calls SFINAE-friendly, since the invalid expression happens in an immediate context, and as such they can be used in expression constraints:
template <std::size_t I, class Tuple>concept has_get = requires(Tuple& t) { get<I>(t); };static_assert(has_get<0, std::tuple<int>> == true, "elem 0: int");static_assert(has_get<1, std::tuple<int>> == false, "out-of-bounds"); |
Given that deleted overloads do participate in overload resolution, even out-of-bounds calls to get will prefer a (deleted) std::get overload to any other user defined viable overload —Murphy, not Machiavelli— when the argument is one of the standard library types:
namespace ud { struct foo { /*...*/ }; template <std::size_t I> void get(std::any thing) { /*gotten*/ }}std::tuple<ud::foo> t;get<0>(t); // okget<1>(t); // error: call to deleted function 'get' // note: declared here // std::get(std::tuple<Ts...>&) = delete; // [with I = 1; Ts = {ud::foo}] |
As an additional side effect, since the error happens only after overload resolution has finished its job, the resulting diagnostics for an out-of-bounds call will only mention the selected (deleted) overload.
4. Proposed Wording
This wording is relative to [N4713].
Change 23.2.1 [utility.syn], Header <utility> synopsis, as indicated:
// 23.4.4, tuple-like access to pairtemplate<classT>classtuple_size;template<size_tI,classT>classtuple_element;template<classT1,classT2>structtuple_size<pair<T1, T2>>;template<size_tI,classT1,classT2>structtuple_element<I, pair<T1, T2>>;template<size_tI,classT1,classT2>constexprtuple_element_t<I, pair<T1, T2>see below& get(pair<T1, T2>&)noexcept;template<size_tI,classT1,classT2>constexprtuple_element_t<I, pair<T1, T2>see below&& get(pair<T1, T2>&&)noexcept;template<size_tI,classT1,classT2>constexprconsttuple_element_t<I, pair<T1, T2>see below& get(constpair<T1, T2>&)noexcept;template<size_tI,classT1,classT2>constexprconsttuple_element_t<I, pair<T1, T2>see below&& get(constpair<T1, T2>&&)noexcept;template<classT1,classT2>constexprT1& get(pair<T1, T2>& p)noexcept;template<classT1,classT2>constexprconstT1& get(constpair<T1, T2>& p)noexcept;template<classT1,classT2>constexprT1&& get(pair<T1, T2>&& p)noexcept;template<classT1,classT2>constexprconstT1&& get(constpair<T1, T2>&& p)noexcept;template<classT2,classT1>constexprT2& get(pair<T1, T2>& p)noexcept;template<classT2,classT1>constexprconstT2& get(constpair<T1, T2>& p)noexcept;template<classT2,classT1>constexprT2&& get(pair<T1, T2>&& p)noexcept;template<classT2,classT1>constexprconstT2&& get(constpair<T1, T2>&& p)noexcepttemplate<classT,classT1,classT2>constexprT& get(pair<T1, T2>& p)noexcept;template<classT,classT1,classT2>constexprconstT& get(constpair<T1, T2>& p)noexcept;template<classT,classT1,classT2>constexprT&& get(pair<T1, T2>&& p)noexcept;template<classT,classT1,classT2>constexprconstT&& get(constpair<T1, T2>&& p)noexcept
Change 23.4.4 [pair.astuple], Tuple-like access to pair, as indicated:
template<classT1,classT2>structtuple_size<pair<T1, T2>> : integral_constant<size_t, 2> { };tuple_element<I, pair<T1, T2>>::type-1- Requires:
I < 2. The program is ill-formed ifIis out of bounds.-2- Value: The type
T1ifI == 0, otherwise the typeT2.
template<size_tI,classT1,classT2>constexprtuple_element_t<I, pair<T1, T2>>V& get(pair<T1, T2>& p)noexcept;template<size_tI,classT1,classT2>constexprconsttuple_element_t<I, pair<T1, T2>>V& get(constpair<T1, T2>& p)noexcept;template<size_tI,classT1,classT2>constexprtuple_element_t<I, pair<T1, T2>>V&& get(pair<T1, T2>&& p)noexcept;template<size_tI,classT1,classT2>constexprconsttuple_element_t<I, pair<T1, T2>>V&& get(constpair<T1, T2>&& p)noexcept;-3- Returns: If
I == 0returns a reference top.first; ifI == 1returns a reference top.second; otherwise the program is ill-formed.-?- Remarks: If
I < 2the typeVistuple_element_t<I, pair<T1, T2>>. Otherwise this function is defined as deleted, andVis an unspecified referenceable type.
template<classT1,classT2>constexprT1& get(pair<T1, T2>& p)noexcept;template<classT1,classT2>constexprconstT1& get(constpair<T1, T2>& p)noexcept;template<classT1,classT2>constexprT1&& get(pair<T1, T2>&& p)noexcept;template<classT1,classT2>constexprconstT1&& get(constpair<T1, T2>&& p)noexcept;
-4- Requires:T1andT2are distinct types. Otherwise, the program is ill-formed.
-5- Returns: A reference top.first.
template<classT2,classT1>constexprT2& get(pair<T1, T2>& p)noexcept;template<classT2,classT1>constexprconstT2& get(constpair<T1, T2>& p)noexcept;template<classT2,classT1>constexprT2&& get(pair<T1, T2>&& p)noexcept;template<classT2,classT1>constexprconstT2&& get(constpair<T1, T2>&& p)noexcept;
-6- Requires:T1andT2are distinct types. Otherwise, the program is ill-formed.
-7- Returns: A reference top.second.
template<classT,classT1,classT2>constexprT& get(pair<T1, T2>& p)noexcept;template<classT,classT1,classT2>constexprconstT& get(constpair<T1, T2>& p)noexcept;template<classT,classT1,classT2>constexprT&& get(pair<T1, T2>&& p)noexcept;template<classT,classT1,classT2>constexprconstT&& get(constpair<T1, T2>&& p)noexcept-?- Returns: If
is_same_v<T, T1>istruereturns a reference top.first; ifis_same_v<T, T2>istruereturns a reference top.second.-?- Remarks: This function is defined as deleted unless
T1andT2are distinct types, andTis eitherT1orT2.
Change 23.5.2 [tuple.syn], Header <tuple> synopsis, as indicated:
// 23.5.3.7, element accesstemplate<size_tI,class... Types>constexprtuple_element_t<I, tuple<Types...>>see below& get(tuple<Types...>&)noexcept;template<size_tI,class... Types>constexprtuple_element_t<I, tuple<Types...>>see below&& get(tuple<Types...>&&)noexcept;template<size_tI,class... Types>constexprconsttuple_element_t<I, tuple<Types...>>see below& get(consttuple<Types...>&)noexcept;template<size_tI,class... Types>constexprconsttuple_element_t<I, tuple<Types...>>see below&& get(consttuple<Types...>&&)noexcept;template<classT,class... Types>constexprT& get(tuple<Types...>& t)noexcept;template<classT,class... Types>constexprT&& get(tuple<Types...>&& t)noexcept;template<classT,class... Types>constexprconstT& get(consttuple<Types...>& t)noexcept;template<classT,class... Types>constexprconstT&& get(consttuple<Types...>&& t)noexcept;
Change 23.5.3.7 [tuple.elem], Element access, as indicated:
template<size_tI,class... Types>constexprtuple_element_t<I, tuple<Types...>>V&get(tuple<Types...>& t)noexcept;template<size_tI,class... Types>constexprtuple_element_t<I, tuple<Types...>>V&&get(tuple<Types...>&& t)noexcept;// Note Atemplate<size_tI,class... Types>constexprconsttuple_element_t<I, tuple<Types...>>V&get(consttuple<Types...>& t)noexcept;// Note Btemplate<size_tI,class... Types>constexprconsttuple_element_t<I, tuple<Types...>>V&& get(consttuple<Types...>&& t)noexcept;
-1- Requires:I < sizeof...(Types). The program is ill-formed ifIis out of bounds.-2- Returns: A reference to the
Ith element oft, where indexing is zero-based.-?- Remarks: If
I < sizeof...(Types)the typeVistuple_element_t<I, tuple<Types...>>. Otherwise this function is defined as deleted, andVis an unspecified referenceable type.-3- [Note A: If a
TinTypesis some reference typeX&, the return type isX&, notX&&. However, if the element type is a non-reference typeT, the return type isT&&. -end note]-4- [Note B: Constness is shallow. If a
TinTypesis some reference typeX&, the return type isX&, notconst X&. However, if the element type is a non-reference typeT, the return type is constT&. This is consistent with how constness is defined to work for member variables of reference type. -end note]
template<classT,class... Types>constexprT& get(tuple<Types...>& t)noexcept;template<classT,class... Types>constexprT&& get(tuple<Types...>&& t)noexcept;template<classT,class... Types>constexprconstT& get(consttuple<Types...>& t)noexcept;template<classT,class... Types>constexprconstT&& get(consttuple<Types...>&& t)noexcept;
-5- Requires: The typeToccurs exactly once inTypes.... Otherwise, the program is ill-formed.-6- Returns: A reference to the element of
tcorresponding to the typeTinTypes....-?- Remarks: This function is defined as deleted unless the type
Toccurs exactly once inTypes....-7- [Example:
consttuple<int,constint,double,double> t(1, 2, 3.4, 5.6);constint& i1 = get<int>(t);// OK. Not ambiguous. i1 == 1constint& i2 = get<constint>(t);// OK. Not ambiguous. i2 == 2constdouble& d = get<double>(t);// ERROR.ill-formeddeleted-end example]
-8- [Note: The reason
getis a non-member function is that if this functionality had been provided as a member function, code where the type depended on a template parameter would have required using thetemplatekeyword. -end note]
Change 26.3.7.6 [utility.syn], Tuple interface to class template array, as indicated:
template<classT,size_tN>structtuple_size<array<T, N>> : integral_constant<size_t, N> { };tuple_element<I, array<T, N>>::type-1- Requires:
I < N. The program is ill-formed ifIis out of bounds.-2- Value: The type
T.
template<size_tI,classT,size_tN>constexprT& get(array<T, N>& a)noexcept;template<size_tI,classT,size_tN>constexprT&& get(array<T, N>&& a)noexcept;template<size_tI,classT,size_tN>constexprconstT& get(constarray<T, N>& a)noexcept;template<size_tI,classT,size_tN>constexprconstT&& get(constarray<T, N>&& a)noexcept;
-3- Requires:I < N. The program is ill-formed ifIis out of bounds.-4- Returns: A reference to the
Ith element ofa, where indexing is zero-based.-?- Remarks: This function is defined as deleted unless
I < N.
Change 23.7.2 [variant.syn], Header <variant> synopsis, as indicated:
// 23.7.5, value accesstemplate<classT,class... Types>constexprboolholds_alternative(constvariant<Types...>&)noexcept;template<size_tI,class... Types>constexprvariant_alternative_t<I, variant<Types...>>see below& get(variant<Types...>&);template<size_tI,class... Types>constexprvariant_alternative_t<I, variant<Types...>>see below&& get(variant<Types...>&&);template<size_tI,class... Types>constexprconstvariant_alternative_t<I, variant<Types...>>see below& get(constvariant<Types...>&);template<size_tI,class... Types>constexprconstvariant_alternative_t<I, variant<Types...>>see below&& get(constvariant<Types...>&&);template<classT,class... Types>constexprT& get(variant<Types...>&);template<classT,class... Types>constexprT&& get(variant<Types...>&&);template<classT,class... Types>constexprconstT& get(constvariant<Types...>&);template<classT,class... Types>constexprconstT&& get(constvariant<Types...>&&);template<size_tI,class... Types>constexpradd_pointer_t<variant_alternative_t<I, variant<Types...>>see below>get_if(variant<Types...>*)noexcept;template<size_tI,class... Types>constexpradd_pointer_t<constvariant_alternative_t<I, variant<Types...>>see below>get_if(constvariant<Types...>*)noexcept;template<classT,class... Types>constexpradd_pointer_t<T>get_if(variant<Types...>*)noexcept;template<classT,class... Types>constexpradd_pointer_t<constT>get_if(constvariant<Types...>*)noexcept;
Change 23.7.5 [variant.get], Value access, as indicated:
template<classT,class... Types>constexprboolholds_alternative(constvariant<Types...>& v)noexcept;-1- Requires: The type
Toccurs exactly once inTypes.... Otherwise, the program is ill-formed.-2- Returns:
trueifindex()is equal to the zero-based index ofTinTypes....
template<size_tI,class... Types>constexprvariant_alternative_t<I, variant<Types...>>V& get(variant<Types...>& v);template<size_tI,class... Types>constexprvariant_alternative_t<I, variant<Types...>>V&& get(variant<Types...>&& v);template<size_tI,class... Types>constexprconstvariant_alternative_t<I, variant<Types...>>V& get(constvariant<Types...>& v);template<size_tI,class... Types>constexprconstvariant_alternative_t<I, variant<Types...>>V&& get(constvariant<Types...>&& v);
-3- Requires:I < sizeof...(Types). Otherwise the program is ill-formed.-4- Effects: If
v.index()isI, returns a reference to the object stored in thevariant. Otherwise, throws an exception of typebad_variant_access.-?- Remarks: If
I < sizeof...(Types)the typeVisvariant_alternative_t<I, variant<Types...>>. Otherwise this function is defined as deleted, andVis an unspecified referenceable type.
template<classT,class... Types>constexprT& get(variant<Types...>& v);template<classT,class... Types>constexprT&& get(variant<Types...>&& v);template<classT,class... Types>constexprconstT& get(constvariant<Types...>& v);template<classT,class... Types>constexprconstT&& get(constvariant<Types...>&& v);
-5- Requires: The typeToccurs exactly once inTypes.... Otherwise, the program is ill-formed.-6- Effects: If v holds a value of type T, returns a reference to that value. Otherwise, throws an exception of type
bad_variant_access.-?- Remarks: This function is defined as deleted unless the type
Toccurs exactly once inTypes....
template<size_tI,class... Types>constexpradd_pointer_t<variant_alternative_t<I, variant<Types...>>V>get_if(variant<Types...>* v)noexcept;template<size_tI,class... Types>constexpradd_pointer_t<constvariant_alternative_t<I, variant<Types...>>V>get_if(constvariant<Types...>* v)noexcept;
-7- Requires:I < sizeof...(Types). Otherwise the program is ill-formed.-8- Returns: A pointer to the value stored in the variant, if
v != nullptrandv->index() == I. Otherwise, returnsnullptr.-?- Remarks: If
I < sizeof...(Types)the typeVisvariant_alternative_t<I, variant<Types...>>. Otherwise this function is defined as deleted, andVis an unspecified referenceable type.
template<classT,class... Types>constexpradd_pointer_t<T>get_if(variant<Types...>* v)noexcept;template<classT,class... Types>constexpradd_pointer_t<constT>get_if(constvariant<Types...>* v)noexcept;
-9- Requires: The typeToccurs exactly once inTypes.... Otherwise, the program is ill-formed.-10- Effects: Equivalent to:
return get_if<i>(v); with i being the zero-based index ofTinTypes....-?- Remarks: This function is defined as deleted unless the type
Toccurs exactly once inTypes....
5. References
-
[N4687] ISO/IEC JTC1 SC22 WG21, Programming Languages - C++, working draft, July 2017
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4687.pdf -
[LWG2974] Diagnose out of bounds
tuple_element/variant_alternative- Agustín Bergé
http://cplusplus.github.io/LWG/lwg-defects.html#2974
A. Alternative Designs
A.1 SFINAE-friendly
A traditional SFINAE-friendly implementation will get out of the user's way when used with an out-of-bounds index. The main disadvantage of this approach is that by not participating in overload resolution, it opens the door for user defined overloads even when called on a standard library tuple-like type; that is, given t of type std::tuple<UDT>, get<1>(t) might silently fall back to a get overload in an associated namespace of UDT. This regresses key functionality in the current std::get design, which mandates a diagnostic for out-of-bounds calls.
template <std::size_t I, typename ...Ts, typename Enable = std::enable_if_t<I < sizeof...(Ts)>>std::tuple_element_t<I, std::tuple<Ts...>>&get(std::tuple<Ts...>& t) { return /*...*/;}// Out-of-bounds calls:std::tuple<int> t;std::get<1>(t); // error: no matching function for call to 'get<1>(std::tuple<int>&)' // note: candidate template ignored: // std::get(array<Ts...>&) // could not match 'array' against 'tuple' // note: candidate template ignored: // std::get(pair<Ts...>&) // could not match 'pair' against 'tuple' // note: candidate template ignored: // std::get(variant<Ts...>&) // could not match 'variant' against 'tuple' // note: candidate template ignored: // std::get(std::tuple<Ts...>&) // [with I = 1; Ts = {int}] // requirement 'I < sizeof...(Ts)' was not satisfied |
Making std::tuple_element SFINAE-friendly would have the same effect, while leaving existing std::get by-index overloads unchanged.
A note on Concepts
A traditional Concept-based implementation is essentially equivalent to a traditional SFINAE-friendly implementation, and so it shares the same disadvantages. That includes the diagnostics generated for an out-of-bounds calls (for currently available implementations).
template <std::size_t I, typename ...Ts> requires I < sizeof...(Ts)std::tuple_element_t<I, std::tuple<Ts...>>&get(std::tuple<Ts...>& t) { return /*...*/;} |
[Note: The current specification unintentionally requires std::tuple_element_t<I, std::tuple<Ts...>> be instantiated before checking that the associated constraints are satisfied, resulting in an ill-formed program for out-of-bounds calls. The results presented here circumvent this issue, under the assumption that it will be rectified. CWG-issue-pending-publication. ]
A.2 Conditionally Deleted
A conditionally deleted implementation prevents the unintended fall back behavior of the traditional SFINAE-friendly approach, while still remaining SFINAE-friendly. As a bonus, diagnostics on out-of-bounds calls tend to be concise.
template <std::size_t I, typename ...Ts, typename Enable = std::enable_if_t<I < sizeof...(Ts)>>std::tuple_element_t<I, std::tuple<Ts...>>&get(std::tuple<Ts...>& t) { return /*...*/;}template <std::size_t I, typename ...Ts>std::enable_if_t<sizeof...(Ts) <= I>get(std::tuple<Ts...>& t) = delete;// Out-of-bounds calls:std::tuple<int> t;std::get<1>(t); // error: call to deleted function 'get' // note: declared here // std::get(std::tuple<Ts...>&) = delete; // [with I = 1; Ts = {int}] |
A.3 Deduced Return Type
An implementation that uses deduced return types can defer the required diagnostic until the definition is instantiated. The main disadvantage is that such definition may need to be instantiated earlier/more often than an explicitly typed alternative, and that the result is SFINAE-unfriendly in those contexts. On the other side, diagnostics on out-of-bounds calls tend to be concise and could include a custom tailored message.
template <std::size_t I, typename ...Ts>decltype(auto) get(std::tuple<Ts...>& t) { static_assert(I < sizeof...(Ts), "tuple index is in range"); return /*...*/;}// Out-of-bounds calls:std::tuple<int> t;std::get<1>(t); // error: static assertion failed: tuple index is in range // in instantiation of // std::get(std::tuple<Ts...>&) // [with I = 1; Ts = {int}] |