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.

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_tI, classTuple>

concepthas_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:

namespaceud {

structfoo { /*...*/};

template<std::size_tI>

voidget(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.

-1- Requires: I < sizeof...(Types). The program is ill-formed if I is out of bounds.

-2- Returns: A reference to the Ith element of t, where indexing is zero-based.

-?- Remarks: If I < sizeof...(Types) the type V is tuple_element_t<I, tuple<Types...>>. Otherwise this function is defined as deleted, and V is an unspecified referenceable type.

-3- [Note A: If a T in Types is some reference type X&, the return type is X&, not X&&. However, if the element type is a non-reference type T, the return type is T&&. -end note]

-4- [Note B: Constness is shallow. If a T in Types is some reference type X&, the return type is X&, not const X&. However, if the element type is a non-reference type T, the return type is const T&. 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 type T occurs exactly once in Types.... Otherwise, the program is ill-formed.

-6- Returns: A reference to the element of t corresponding to the type T in Types....

-?- Remarks: This function is defined as deleted unless the type T occurs exactly once in Types....

-7- [Example:

consttuple<int, constint, double, double> t(1, 2, 3.4, 5.6);

constint& i1 = get<int>(t); // OK. Not ambiguous. i1 == 1

constint& i2 = get<constint>(t); // OK. Not ambiguous. i2 == 2

constdouble& 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 the template keyword. -end note]

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_tI, typename...Ts,

typenameEnable = 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 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_tI, typename...Ts>

requiresI < 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 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.

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.