Date: 2017-10-12
Project: Programming Language C++, Library Evolution Working Group
Reply-to: Agustín Bergé
[email protected]
A friendlier tuple
get
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 :
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);
get<1>(wt);
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
3.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 bound access.
Possible implementation:
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
;
std::tuple
<
int
> t;
std::get
<1>(t);
3.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 bound access tend to be concise.
Possible implementation:
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
;
std::tuple
<
int
> t;
std::get
<1>(t);
3.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 bound access tend to be concise and could include a custom tailored message.
Possible implementation:
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
;
std::tuple
<
int
> t;
std::get
<1>(t);
-Y-
Requires
: The type
T
occurs exactly once in
Types...
. Otherwise, the program is ill-formed.
-?-
Remarks
: This function shall not participate in overload resolution unless the type
T
occurs exactly once in
Types...
.
4.2 Option B, Conditionally Deleted
-Y-
Requires
: The type
T
occurs exactly once in
Types...
. Otherwise, the program is ill-formed.
-?-
Remarks
: This function shall be defined as deleted unless the type
T
occurs exactly once in
Types...
.
4.3 Option C, Deduced Return Type
template
<
size_t
I,
class
... Types>
constexpr
tuple_element_t<I, tuple<Types...>>&
decltype
(
auto
)
get(tuple<Types...>& t)
noexcept
;
-Y-
Requires
: The type
T
occurs exactly once in
Types...
. Otherwise, the program is ill-formed.
5. References