Document number: | P1951R0 |
Date: | 2019-11-17 |
Audience: | LWG |
Reply-to: | Logan R. Smith <[email protected]> |
pair
's Forwarding ConstructorIntroduction
Motivation
Impact
Proposed Changes
Acknowledgements
This paper proposes defaulting the template arguments U1
and U2
in pair
's
forwarding constructor to
T1
and
T2
respectively, so that braced initializers may be used as constructor arguments to it.
Consider this innocent-looking construction of a std::pair
:
std::pair<std::string, std::vector<std::string>> p("hello", {});
This code uses simple, highly natural syntax for the constructor arguments; it is what any C++ programmer, beginner or expert, would hope to be able to write. During constructor overload resolution, two two-argument constructors (or constructor templates) are considered:
(A) EXPLICIT constexpr pair(const T1& x, const T2& y);
and
(B) template<class U1, class U2> EXPLICIT constexpr pair(U1&& x, U2&& y);
The more efficient option, and the one the user likely hoped would be used, is (B), which takes two forwarding
references and perfectly forwards them to the constructors of first
and second
. However,
since the second
argument to the constructor was given as {}
, the type of U2
cannot be deduced, and so (B)
is
removed from
overload resolution and (A) is selected. From there, a temporary std::string
and
std::vector<std::string>
are
created at the call site, and are passed by const reference to be copied into first
and
second
. Thus, the
simplest and easiest code to write results in potentially very inefficient behavior.
(Note, by contrast, if the pair is constructed using the similar-in-spirit
p("hello", std::vector<std::string>{})
,
then
(B) is selected, since U2
can be deduced in this case. This subtlety of syntax is surprising and
user-unfriendly.)
If (B)'s template arguments were adjusted slightly to default to the pair's first and second type, respectively, there would be a fallback when deduction of braced initializers fails. Using the following adjusted signature:
(C) template<class U1=T1, class U2=T2> EXPLICIT constexpr pair(U1&& x, U2&& y);
this overload can now be selected for the example case above, so the example pair's string member is constructed in-place from a perfectly-forwarded string literal, and its vector member is move-constructed instead of copy-constructed from the temporary vector at the call site.
There is precedent in the standard library for using default template arguments specifically to accommodate
braced initializers; for instance, std::optional
's forwarding constructor, and the second parameter of
std::exchange
. This paper recommends std::pair
adopt this same strategy, for reducing
surprise and
making the
most natural syntax be acceptably efficient.
This change would alter the meaning of existing code that uses braced initializers in the construction of pairs, likely changing copies to moves or perfectly-forwarded constructions in many cases. It is all but certain that this new behavior would be welcomed over the old.
Note that this proposal has no effect on APIs such as std::map::emplace
which forward constructor
arguments to pair
, since those APIs need to deduce all argument types on their own first before
forwarding them. Code such as
std::map<std::string, std::vector<std::string>> m; m.emplace("hello", {});
remains ill-formed after this proposal.
template<class U1, class U2> EXPLICIT constexpr pair(U1&& x, U2&& y);
template<class U1=T1, class U2=T2> EXPLICIT constexpr pair(U1&& x, U2&& y);
Likewise, change the declaration around 23.4.2 [pairs.pair] p7:
template<class U1, class U2> _EXPLICIT_ constexpr pair(U1&& x, U2&& y);
template<class U1=T1, class U2=T2> EXPLICIT constexpr pair(U1&& x, U2&& y);
Thanks to Narut Sereewattanawoot for helping discuss this problem and work out this solution.