Document number: P0825R0
Date: 2017-10-12
Project: Programming Language C++, Library Evolution Working Group
Reply-to: Agustín Bergé agustinberge@gmail.com

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); // 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 aluded 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

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 /*...*/;
}

Out of bounds access:

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

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;

Out of bounds access:

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}]

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 /*...*/;
}

Out of bounds access:

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}]

4. Proposed Wording Preview (informative)

This wording is relative to [N4687].

4.1 Option A, SFINAE-friendly

template <size_t I, class... Types>
  constexpr tuple_element_t<I, tuple<Types...>>&
    get(tuple<Types...>& t) noexcept;

-X- Requires: I < sizeof...(Types). Otherwise, the program is ill-formed.

-?- Remarks: This function shall not participate in overload resolution unless I < sizeof...(Types).

template <class T, class... Types>
  constexpr T& get(tuple<Types...>& t) noexcept;

-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

template <size_t I, class... Types>
  constexpr tuple_element_t<I, tuple<Types...>>&
    get(tuple<Types...>& t) noexcept;

-X- Requires: I < sizeof...(Types). Otherwise, the program is ill-formed.

-?- Remarks: This function shall be defined as deleted unless I < sizeof...(Types).

template <class T, class... Types>
  constexpr T& get(tuple<Types...>& t) noexcept;

-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;

-X- Requires: I < sizeof...(Types). Otherwise, the program is ill-formed.

template <class T, class... Types>
  constexpr T&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