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 10 get<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); // ok get<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 pair
template
<
class
T>
class
tuple_size;
template
<
size_t
I,
class
T>
class
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>>;
template
<
size_t
I,
class
T1,
class
T2>
constexpr
tuple_element_t<I, pair<T1, T2>see below& get(pair<T1, T2>&)noexcept
;
template
<
size_t
I,
class
T1,
class
T2>
constexpr
tuple_element_t<I, pair<T1, T2>see below&& get(pair<T1, T2>&&)noexcept
;
template
<
size_t
I,
class
T1,
class
T2>
constexpr
const
tuple_element_t<I, pair<T1, T2>see below& get(const
pair<T1, T2>&)
noexcept
;
template
<
size_t
I,
class
T1,
class
T2>
constexpr
const
tuple_element_t<I, pair<T1, T2>see below&& get(const
pair<T1, T2>&&)
noexcept
;
template
<
class
T1,
class
T2>
constexpr
T1& get(pair<T1, T2>& p)
noexcept
;
template
<
class
T1,
class
T2>
constexpr
const
T1& get(
const
pair<T1, T2>& p)
noexcept
;
template
<
class
T1,
class
T2>
constexpr
T1&& get(pair<T1, T2>&& p)
noexcept
;
template
<
class
T1,
class
T2>
constexpr
const
T1&& get(
const
pair<T1, T2>&& p)
noexcept
;
template
<
class
T2,
class
T1>
constexpr
T2& get(pair<T1, T2>& p)
noexcept
;
template
<
class
T2,
class
T1>
constexpr
const
T2& get(
const
pair<T1, T2>& p)
noexcept
;
template
<
class
T2,
class
T1>
constexpr
T2&& get(pair<T1, T2>&& p)
noexcept
;
template
<
class
T2,
class
T1>
constexpr
const
T2&& get(
const
pair<T1, T2>&& p)
noexcept
template
<
class
T,
class
T1,
class
T2>
constexpr
T& get(pair<T1, T2>& p)
noexcept
;
template
<
class
T,
class
T1,
class
T2>
constexpr
const
T& get(
const
pair<T1, T2>& p)
noexcept
;
template
<
class
T,
class
T1,
class
T2>
constexpr
T&& get(pair<T1, T2>&& p)
noexcept
;
template
<
class
T,
class
T1,
class
T2>
constexpr
const
T&& get(
const
pair<T1, T2>&& p)
noexcept
Change 23.4.4 [pair.astuple], Tuple-like access to pair
, as indicated:
template
<
class
T1,
class
T2>
struct
tuple_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 ifI
is out of bounds.-2- Value: The type
T1
ifI == 0
, otherwise the typeT2
.
template
<
size_t
I,
class
T1,
class
T2>
constexpr
tuple_element_t<I, pair<T1, T2>>V& get(pair<T1, T2>& p)noexcept
;
template
<
size_t
I,
class
T1,
class
T2>
constexpr
const
tuple_element_t<I, pair<T1, T2>>V& get(const
pair<T1, T2>& p)
noexcept
;
template
<
size_t
I,
class
T1,
class
T2>
constexpr
tuple_element_t<I, pair<T1, T2>>V&& get(pair<T1, T2>&& p)noexcept
;
template
<
size_t
I,
class
T1,
class
T2>
constexpr
const
tuple_element_t<I, pair<T1, T2>>V&& get(const
pair<T1, T2>&& p)
noexcept
;
-3- Returns: If
I == 0
returns a reference top.first
; ifI == 1
returns a reference top.second
; otherwise the program is ill-formed.-?- Remarks: If
I < 2
the typeV
istuple_element_t<I, pair<T1, T2>>
. Otherwise this function is defined as deleted, andV
is an unspecified referenceable type.
template
<
class
T1,
class
T2>
constexpr
T1& get(pair<T1, T2>& p)
noexcept
;
template
<
class
T1,
class
T2>
constexpr
const
T1& get(
const
pair<T1, T2>& p)
noexcept
;
template
<
class
T1,
class
T2>
constexpr
T1&& get(pair<T1, T2>&& p)
noexcept
;
template
<
class
T1,
class
T2>
constexpr
const
T1&& get(
const
pair<T1, T2>&& p)
noexcept
;
-4- Requires:T1
andT2
are distinct types. Otherwise, the program is ill-formed.
-5- Returns: A reference top.first
.
template
<
class
T2,
class
T1>
constexpr
T2& get(pair<T1, T2>& p)
noexcept
;
template
<
class
T2,
class
T1>
constexpr
const
T2& get(
const
pair<T1, T2>& p)
noexcept
;
template
<
class
T2,
class
T1>
constexpr
T2&& get(pair<T1, T2>&& p)
noexcept
;
template
<
class
T2,
class
T1>
constexpr
const
T2&& get(
const
pair<T1, T2>&& p)
noexcept
;
-6- Requires:T1
andT2
are distinct types. Otherwise, the program is ill-formed.
-7- Returns: A reference top.second
.
template
<
class
T,
class
T1,
class
T2>
constexpr
T& get(pair<T1, T2>& p)
noexcept
;
template
<
class
T,
class
T1,
class
T2>
constexpr
const
T& get(
const
pair<T1, T2>& p)
noexcept
;
template
<
class
T,
class
T1,
class
T2>
constexpr
T&& get(pair<T1, T2>&& p)
noexcept
;
template
<
class
T,
class
T1,
class
T2>
constexpr
const
T&& get(
const
pair<T1, T2>&& p)
noexcept
-?- Returns: If
is_same_v<T, T1>
istrue
returns a reference top.first
; ifis_same_v<T, T2>
istrue
returns a reference top.second
.-?- Remarks: This function is defined as deleted unless
T1
andT2
are distinct types, andT
is eitherT1
orT2
.
Change 23.5.2 [tuple.syn], Header <tuple>
synopsis, as indicated:
// 23.5.3.7, element access
template
<
size_t
I,
class
... Types>
constexpr
tuple_element_t<I, tuple<Types...>>see below& get(tuple<Types...>&)noexcept
;
template
<
size_t
I,
class
... Types>
constexpr
tuple_element_t<I, tuple<Types...>>see below&& get(tuple<Types...>&&)noexcept
;
template
<
size_t
I,
class
... Types>
constexpr
const
tuple_element_t<I, tuple<Types...>>see below& get(const
tuple<Types...>&)
noexcept
;
template
<
size_t
I,
class
... Types>
constexpr
const
tuple_element_t<I, tuple<Types...>>see below&& get(const
tuple<Types...>&&)
noexcept
;
template
<
class
T,
class
... Types>
constexpr
T& get(tuple<Types...>& t)
noexcept
;
template
<
class
T,
class
... Types>
constexpr
T&& get(tuple<Types...>&& t)
noexcept
;
template
<
class
T,
class
... Types>
constexpr
const
T& get(
const
tuple<Types...>& t)
noexcept
;
template
<
class
T,
class
... Types>
constexpr
const
T&& get(
const
tuple<Types...>&& t)
noexcept
;
Change 23.5.3.7 [tuple.elem], Element access, as indicated:
template
<
size_t
I,
class
... Types>
constexpr
tuple_element_t<I, tuple<Types...>>V&
get(tuple<Types...>& t)
noexcept
;
template
<
size_t
I,
class
... Types>
constexpr
tuple_element_t<I, tuple<Types...>>V&&
get(tuple<Types...>&& t)
noexcept
;
// Note A
template
<
size_t
I,
class
... Types>
constexpr
const
tuple_element_t<I, tuple<Types...>>V&
get(
const
tuple<Types...>& t)
noexcept
;
// Note B
template
<
size_t
I,
class
... Types>
constexpr
const
tuple_element_t<I, tuple<Types...>>V&& get(const
tuple<Types...>&& t)
noexcept
;
-1- Requires:I < sizeof...(Types)
. The program is ill-formed ifI
is out of bounds.-2- Returns: A reference to the
I
th element oft
, where indexing is zero-based.-?- Remarks: If
I < sizeof...(Types)
the typeV
istuple_element_t<I, tuple<Types...>>
. Otherwise this function is defined as deleted, andV
is an unspecified referenceable type.-3- [Note A: If a
T
inTypes
is 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
T
inTypes
is 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
<
class
T,
class
... Types>
constexpr
T& get(tuple<Types...>& t)
noexcept
;
template
<
class
T,
class
... Types>
constexpr
T&& get(tuple<Types...>&& t)
noexcept
;
template
<
class
T,
class
... Types>
constexpr
const
T& get(
const
tuple<Types...>& t)
noexcept
;
template
<
class
T,
class
... Types>
constexpr
const
T&& get(
const
tuple<Types...>&& t)
noexcept
;
-5- Requires: The typeT
occurs exactly once inTypes...
. Otherwise, the program is ill-formed.-6- Returns: A reference to the element of
t
corresponding to the typeT
inTypes...
.-?- Remarks: This function is defined as deleted unless the type
T
occurs exactly once inTypes...
.-7- [Example:
const
tuple<
int
,
const
int
,
double
,
double
> t(1, 2, 3.4, 5.6);
const
int
& i1 = get<
int
>(t);
// OK. Not ambiguous. i1 == 1
const
int
& i2 = get<
const
int
>(t);
// OK. Not ambiguous. i2 == 2
const
double
& d = get<
double
>(t);
// ERROR.
ill-formeddeleted-end example]
-8- [Note: The reason
get
is 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 thetemplate
keyword. -end note]
Change 26.3.7.6 [utility.syn], Tuple interface to class template array
, as indicated:
template
<
class
T,
size_t
N>
struct
tuple_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 ifI
is out of bounds.-2- Value: The type
T
.
template
<
size_t
I,
class
T,
size_t
N>
constexpr
T& get(array<T, N>& a)
noexcept
;
template
<
size_t
I,
class
T,
size_t
N>
constexpr
T&& get(array<T, N>&& a)
noexcept
;
template
<
size_t
I,
class
T,
size_t
N>
constexpr
const
T& get(
const
array<T, N>& a)
noexcept
;
template
<
size_t
I,
class
T,
size_t
N>
constexpr
const
T&& get(
const
array<T, N>&& a)
noexcept
;
-3- Requires:I < N
. The program is ill-formed ifI
is out of bounds.-4- Returns: A reference to the
I
th 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 access
template
<
class
T,
class
... Types>
constexpr
bool
holds_alternative(
const
variant<Types...>&)
noexcept
;
template
<
size_t
I,
class
... Types>
constexpr
variant_alternative_t<I, variant<Types...>>see below& get(variant<Types...>&);template
<
size_t
I,
class
... Types>
constexpr
variant_alternative_t<I, variant<Types...>>see below&& get(variant<Types...>&&);template
<
size_t
I,
class
... Types>
constexpr
const
variant_alternative_t<I, variant<Types...>>see below& get(const
variant<Types...>&);
template
<
size_t
I,
class
... Types>
constexpr
const
variant_alternative_t<I, variant<Types...>>see below&& get(const
variant<Types...>&&);
template
<
class
T,
class
... Types>
constexpr
T& get(variant<Types...>&);
template
<
class
T,
class
... Types>
constexpr
T&& get(variant<Types...>&&);
template
<
class
T,
class
... Types>
constexpr
const
T& get(
const
variant<Types...>&);
template
<
class
T,
class
... Types>
constexpr
const
T&& get(
const
variant<Types...>&&);
template
<
size_t
I,
class
... Types>
constexpr
add_pointer_t<
variant_alternative_t<I, variant<Types...>>see below>
get_if(variant<Types...>*)
noexcept
;
template
<
size_t
I,
class
... Types>
constexpr
add_pointer_t<
const
variant_alternative_t<I, variant<Types...>>see below>
get_if(
const
variant<Types...>*)
noexcept
;
template
<
class
T,
class
... Types>
constexpr
add_pointer_t<T>
get_if(variant<Types...>*)
noexcept
;
template
<
class
T,
class
... Types>
constexpr
add_pointer_t<
const
T>
get_if(
const
variant<Types...>*)
noexcept
;
Change 23.7.5 [variant.get], Value access, as indicated:
template
<
class
T,
class
... Types>
constexpr
bool
holds_alternative(
const
variant<Types...>& v)
noexcept
;
-1- Requires: The type
T
occurs exactly once inTypes...
. Otherwise, the program is ill-formed.-2- Returns:
true
ifindex()
is equal to the zero-based index ofT
inTypes...
.
template
<
size_t
I,
class
... Types>
constexpr
variant_alternative_t<I, variant<Types...>>V& get(variant<Types...>& v);template
<
size_t
I,
class
... Types>
constexpr
variant_alternative_t<I, variant<Types...>>V&& get(variant<Types...>&& v);template
<
size_t
I,
class
... Types>
constexpr
const
variant_alternative_t<I, variant<Types...>>V& get(const
variant<Types...>& v);
template
<
size_t
I,
class
... Types>
constexpr
const
variant_alternative_t<I, variant<Types...>>V&& get(const
variant<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 typeV
isvariant_alternative_t<I, variant<Types...>>
. Otherwise this function is defined as deleted, andV
is an unspecified referenceable type.
template
<
class
T,
class
... Types>
constexpr
T& get(variant<Types...>& v);
template
<
class
T,
class
... Types>
constexpr
T&& get(variant<Types...>&& v);
template
<
class
T,
class
... Types>
constexpr
const
T& get(
const
variant<Types...>& v);
template
<
class
T,
class
... Types>
constexpr
const
T&& get(
const
variant<Types...>&& v);
-5- Requires: The typeT
occurs 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
T
occurs exactly once inTypes...
.
template
<
size_t
I,
class
... Types>
constexpr
add_pointer_t<
variant_alternative_t<I, variant<Types...>>V>
get_if(variant<Types...>* v)
noexcept
;
template
<
size_t
I,
class
... Types>
constexpr
add_pointer_t<
const
variant_alternative_t<I, variant<Types...>>V>
get_if(
const
variant<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 != nullptr
andv->index() == I
. Otherwise, returnsnullptr
.-?- Remarks: If
I < sizeof...(Types)
the typeV
isvariant_alternative_t<I, variant<Types...>>
. Otherwise this function is defined as deleted, andV
is an unspecified referenceable type.
template
<
class
T,
class
... Types>
constexpr
add_pointer_t<T>
get_if(variant<Types...>* v)
noexcept
;
template
<
class
T,
class
... Types>
constexpr
add_pointer_t<
const
T>
get_if(
const
variant<Types...>* v)
noexcept
;
-9- Requires: The typeT
occurs 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 ofT
inTypes...
.-?- Remarks: This function is defined as deleted unless the type
T
occurs 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}] |