Improving pair and tuple, revision 3

LWG 2312 is partially addressed: The currently
proposed wording still intends to make an implementation conforming that would accept a smaller number of elements values compared to the
actual tuple-size (and to value-initialize the remaining ones), therefore the size-related requirements are currently not part of the
SFINAE constraints. If LWG 2312 would be accepted the corresponding constraints would be similarly adjusted as described by the issue
and the corresponding requirement could be removed.

and none of these can be explicit as declared (The core language does not propagate explicit for
special members when declared in that form). The whole idea of propagating EXPLICIT to the
allocator constructors was to reflect the explicit-nature of the corresponding non-tuple constructors. To
realize this, EXPLICIT need to be removed from the three aforementioned constructors.

This paper explains the reason for the current specification of pair and tuple and suggests
changes to the working paper to fix these and some other bothersome constraints of these general type wrappers.

Looking backwards

At the time when N3240 was
proposed, several driving forces defined the constraints of resolving a bunch of library issues and NB
comments.

One notable intention was to prevent that the type wrappers tuple and pair should allow
implicit conversions for wrapped types that are not implicitly convertible, as expressed by
LWG 1324 and
DE 15.

Another relevant requirement was to keep backward-compatibility to C++03 in regard to null pointer literals
expressed as integral null constants as described by
LWG 811.

At that time there was a strong resistance to add further constructors especially to std::pair. At some point in
time there did exist a very large number of such constructors due to allocator support. One important consequence
of this pair simplification was the acceptance of N3059.

Thus with the previous acceptance of proposal N3240
the specification provides the following advantages for pair and tuple:

Heterogenous template constructors are constrained upon the criterion that the element-wise source types are
implicitly convertible to the element-wise destination types.

The non-template constructor tuple(const Types&...) and the corresponding template-constructor are
explicit. This prevents that a single-element tuple from being copy-initialized by an
argument object and has only an explicit constructor for this construction.

Non-template constructors accepting a sequence of elements, such as explicit tuple(const Types&...)
and pair(const T1& x, const T2& y), are still kept to support the special conversion scenario where a
pointer(-to-member) element type is initialized with the null pointer constant 0 instead of nullptr.

Discussion

We propose that pair/tuple obey the same initialization rules as their underlying element types. Unless we have
such "perfect initialization", pair and tuple exhibit confusing and unintuitive behavior as illustrated
by the examples below.

It means that tuple objects cannot be returned from a function as simple as this:

It even means that tuple or pair objects cannot be direct-constructed for element types via an explicit
conversion:

struct Y { explicit Y(int); };
std::tuple<Y> ty(12); // Error

It has been observed by Johannes Schaub
that there exists a defect with tuple in regard to the
non-template constructor explicit tuple(const Types&...): The current specification has the effect that
the instantiation of tuple<> would be required to be ill-formed because it has two conflicting default
constructors.

Starting with the last point: This is indeed a simple oversight that slipped in during the tuple standardization.
The TR1 document did have the following specification:

The shown constructors both use perfect forwarding
and they have essentially the same signatures except for one being explicit, the other one not. Furthermore, they are
mutually exclusively constrained. In other words: This combination behaves for any destination type T and any
argument type U like a single constructor that is either explicit or non-explicit (or no constructor
at all). Attempts to construct a A<T> from some value of type U will reflect the allowed initialization forms
of the wrapped type T:

This technique can easily be extended to the variadic template case, and when doing so can be considered as a key to solving
the problems of tuple and pair.

It should be noted here, that for the general case the std::is_constructible<T, U>::value requirement for the
non-explicit constructor which is constrained on std::is_convertible<U, T>::value is not redundant, because it
is possible to create types that can be copy-initialized but not direct-initialized:

Technically it would be possible to apply the same technique of creating element-dependent explicit or non-explicit default
constructors. This application was shortly considered during the write-up of this proposal, but rejected because for the
current C++ rules there is no longer any observable difference for an explicit default constructor that cannot be
invoked with more than zero arguments and one that is not explicit.

The technique cannot be applied to copy/move operators in the same way as for the other constructors (because these special
member functions cannot be templates) and given the very rare request for such a support the idea was no further investigated
by the author. A second argument against providing this support is based on the consistency with the core language rules
that the explicit-character of these constructors is not conserved for the implicitly declared versions in classes
that contain corresponding sub-objects with such explicit constructors.

Outline

As shown above, the current over-constraining restrictions of the pair and tuple constructors are due to
unconditional usage of explicit and implicitly convertible requirements.

The general approach of this proposal is to require "perfect initialization" semantics for pair and tuple
constructors extended to the variadic case. Albeit this seemingly doubles the number of constructor declarations in the draft,
it does not change the effective number of these for a particular combination of element type and source type of
some initialization due to their mutual exclusion property.

In theory the same technique could be applied to the piecewise_construct_t of pair. This proposal does
not propose this, because this constructor is specifically asked for by the corresponding tag and there are no further
constraint except the is_constructible requirements.

In addition, this proposal fixes the specification problem of tuple<>'s default constructors.

The wording is intentionally chosen, so that an implementation is not required (but allowed) to use the "perfect initialization" idiom.

This is done by taking advantage of the already existing nomenclature "This function does not participate in overload resolution
unless […]". Its worth emphasizing that even though this phrase is usually used to describe constrained templates
in the Library specification, the actual wording of this doesn't necessarily imply to "sfinae out" template functions. Many library
implementations solve this problem by providing a specialization for the empty tuple case that does not provide
the additional default constructor, for example. This is also a valid way to ensure that functions don't participate in overload resolution.

Why not explicit for single argument constructors only?

In C++03 explicit constructors had no behavioural difference, unless they had been single-argument constructors, so
one might suggest to restrict adding the explicit keyword to constructors that take exactly one argument.

I think this is idea is flawed (unless I'm using specifically tagged constructors like the piecewise-one of pair). Consider
the following example:

If the former two calls to function launch_rocket_at where possible, this would directly subvert the
intended explicitness of the std::duration constructor and would make using the time-utility
types much more unsafe. Why? Consider the following scenario:

If the client believed that the order of the units was seconds, minutes, hours, and input 3, 2, 1, —
intending 3 seconds, 2 minutes, and 1 hour — the rocket would launch in 10,921 seconds instead of the intended
3,723 seconds. This mistake can indeed easily happen, if you look again at the lines marked with #1 and #2.

Due to our intentionally conserved constraints to be explicit here we catch that mistake at compile-time,
instead of having to shoot the rocket down...

Editorial Representation

During the write-up of this proposal I had the idea of replacing the prototype declaration pairs by
a single one expressed by some pseudo-macro that looks like a single declaration. For example

This form of representation still means that EXPLICIT needs to be defined somewhere
and somehow, but only once.

There was a strong preference during the Rapperswil 2014 LEWG discussion to use the
"macro" instead of the individual declarations.

This paper does not use a previously suggested form of EXPLICIT(see below) instead of EXPLICIT,
because there is nothing specific to explain below again (The different SFINAE conditions are not relevant at that point,
because they appear as usual with the normal, detailed prototype specifications).

In any way, the final editorial decision is being handed over to the project editor.

Proposed resolution

Add an additional new paragraph at the end of 17.5.2.2 [functions.within.classes] and move the current second paragraph to the end of the first
paragraph:

-1- For the sake of exposition, Clauses 18 through 30 and Annex D do not describe copy/move constructors,
assignment operators, or (non-virtual) destructors with the same apparent semantics as those that can be
generated by default (12.1, 12.4, 12.8). It is unspecified whether the implementation provides explicit
definitions for such member function signatures, or for virtual destructors that can be generated by default.

-2- It is unspecified whether the implementation provides explicit definitions for such member function signatures,
or for virtual destructors that can be generated by default.

-?- For the sake of exposition, the library clauses sometimes annotate constructors with EXPLICIT. Such a
constructor is conditionally declared as either explicit or non-explicit (12.3.1 [class.conv.ctor]). [Note: This is typically
implemented by declaring two such constructors, of which at most one participates in overload resolution — end note]

-5- Requires: is_copy_constructible<first_type>::value is true and
is_copy_constructible<second_type>::value is true.

-6- Effects: The constructor initializes first with x and second with y.

-?- Remarks: This constructor shall not participate in overload resolution unless
is_copy_constructible<first_type>::value is true and
is_copy_constructible<second_type>::value is true. The constructor is explicit if and only if
is_convertible<const first_type&, first_type>::value is false or
is_convertible<const second_type&, second_type>::value is false.

-8- Effects: The constructor initializes first with std::forward<U>(x) and second with
std::forward<V>(y).

-9- Remarks: If U is not implicitly convertible to first_type or V is not implicitly convertible to
second_type this constructor shall not participate in overload resolution.This constructor shall not participate in overload resolution unless is_constructible<first_type, U&&>::value
is true and is_constructible<second_type, V&&>::value is true. The constructor is explicit
if and only if is_convertible<U&&, first_type>::value is false or is_convertible<V&&, second_type>::value
is false.

-11- Effects: The constructor initializesInitializes members from the corresponding members of the argument.

-12- Remarks: This constructor shall not participate in overload resolution unless const U& is
implicitly convertible to first_type and const V& is implicitly convertible to second_typeis_constructible<first_type, const U&>::value is true and
is_constructible<second_type, const V&>::value is true. The constructor is explicit
if and only if is_convertible<const U&, first_type>::value is false or
is_convertible<const V&, second_type>::value is false.

-14- Effects: The constructor initializes first with std::forward<U>(p.first)
and second with std::forward<V>(p.second).

-15- Remarks: This constructor shall not participate in overload resolution unless U is
implicitly convertible to first_type and V is implicitly convertible to second_typeis_constructible<first_type, U&&>::value is true and
is_constructible<second_type, V&&>::value is true. The constructor is explicit
if and only if is_convertible<U&&, first_type>::value is false or
is_convertible<V&&, second_type>::value is false.

Change 20.4.2 [tuple.tuple], class template tuple synopsis, as indicated. The intent is to declare
the set of "conditionally explicit" constructors and to fix the multiple default constructor
problem for empty tuples.

-7- Effects: The constructor initializesInitializes each element with the value of the corresponding parameter.

-?- Remarks: This constructor shall not participate in overload resolution unless sizeof...(Types) >= 1 and
is_copy_constructible<Ti>::value is true for all i. The constructor is explicit
if and only if is_convertible<const Ti&, Ti>::value is false for at least one i.

-9- Effects: The constructor initializesInitializes the elements in the tuple with the corresponding value in std::forward<UTypes>(u).

-10- Remarks: This constructor shall not participate in overload resolution unless each type in UTypes is implicitly convertible
to its corresponding type in Typessizeof...(Types) >= 1 and is_constructible<Ti, Ui&&>::value is true for all i.
The constructor is explicit if and only if is_convertible<Ui&&, Ti>::value is false for
at least one i.

-16- Effects: The constructor initializesConstructs each element of *this with the corresponding element of u.

-17- Remarks: This constructor shall not participate in overload resolution unless
const Ui& is implicitly convertible to Ti for all iis_constructible<Ti, const Ui&>::value is true for all i.
The constructor is explicit if and only if is_convertible<const Ui&, Ti>::value
is false for at least one i.

-19- Effects: For all i, the constructor initializes the ith element of
*this with std::forward<Ui>(get<i>(u)).

-20- Remarks: This constructor shall not participate in overload resolution unless each type in UTypes
is implicitly convertible to its corresponding type in Typesis_constructible<Ti, Ui&&>::value is true for all i.
The constructor is explicit if and only if is_convertible<Ui&&, Ti>::value
is false for at least one i.

-21- Requires: sizeof...(Types) == 2. is_constructible<T0, const U1&>::value is true for
the first type T0 in Types and is_constructible<T1, const U2&>::value
is true for the second type T1 in Types.

-22- Effects: The constructor initializesConstructs the first element with u.first and the second element with u.second.

-23- Remarks: This constructor shall not participate in overload resolution unless const U1&
is implicitly convertible to T0 and const U2& is implicitly convertible to T1is_constructible<T0, const U1&>::value is true and
is_constructible<T1, const U2&>::value is true. The constructor is explicit if and only if
is_convertible<const U1&, T0>::value is false or
is_convertible<const U2&, T1>::value is false.

-24- Requires: sizeof...(Types) == 2. is_constructible<T0, U1&&>::value is true for
the first type T0 in Types and is_constructible<T1, U2&&>::value
is true for the second type T1 in Types.

-25- Effects: The constructor iInitializes the first element with
std::forward<U1>(u.first) and the second element with std::forward<U2>(u.second).

-26- Remarks: This constructor shall not participate in overload resolution unless U1 is implicitly
convertible to T0 and U2 is implicitly convertible to T1is_constructible<T0, U1&&>::value is true and
is_constructible<T1, U2&&>::value is true. The constructor is explicit if and only
if is_convertible<U1&&, T0>::value is false
or is_convertible<U2&&, T1>::value is false.

Acknowledgements

I would like to thank Howard Hinnant for his very helpful discussions and comments during reviews of this paper and for
his motivating example. Thanks also to Jonathan Wakely for his review that improved this proposal to a large extend.
Thanks as well go to Mike Spertus for helping to improve the rationale.