std::pair initialization of non-trivial classes with only one initializer list argument
Asked Answered
W

2

7

Consider the following code:

#include <utility>
#include <vector>

using V = std::vector<int>;

int main() {
    std::pair<int, V> p1{1, 2};   // p1.second has 2 elements
    std::pair<int, V> p2{1, {2}}; // p2.second has 1 element

    std::pair<V, V> p3{2, 2};     // Both vectors have 2 elements
    std::pair<V, V> p4{{2}, {2}}; // Both vectors have 1 element

    std::pair<V, V> p5{2, {2}};   // Does not compile
    // p5.first should have 2 elements, while the other should have 1
}

My main issue is with the last line, p5, which does not compile with g++-12 but does with g++-10. I would like to know:

  • What has changed that caused this issue?
  • Can it be compiled again without having to build the vectors and copying them in (i.e. not use V(2) somewhere)

I have tried playing with std::piecewise_construct as well but I'm not sure it is the correct solution here.

ERROR:

<source>: In function 'int main()':
<source>:9:30: error: no matching function for call to 'std::pair<std::vector<int>, std::vector<int> >::pair(<brace-enclosed initializer list>)'
    9 |     std::pair<V, V> p3{2, {2}};
      |                              ^
In file included from /opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/utility:69,
                 from <source>:1:
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:354:9: note: candidate: 'template<class _U1, class _U2>  requires  _S_constructible<_U1, _U2>() && _S_dangles<_U1, _U2>() constexpr std::pair<_T1, _T2>::pair(std::pair<_U1, _U2>&&) [with _U2 = _U1; _T1 = std::vector<int>; _T2 = std::vector<int>]' (deleted)
  354 |         pair(pair<_U1, _U2>&&) = delete;
      |         ^~~~
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:354:9: note:   template argument deduction/substitution failed:
<source>:9:30: note:   mismatched types 'std::pair<_T1, _T2>' and 'int'
    9 |     std::pair<V, V> p3{2, {2}};
      |                              ^
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:345:9: note: candidate: 'template<class _U1, class _U2>  requires  _S_constructible<_U1, _U2>() && !_S_dangles<_U1, _U2>() constexpr std::pair<_T1, _T2>::pair(std::pair<_U1, _U2>&&) [with _U2 = _U1; _T1 = std::vector<int>; _T2 = std::vector<int>]'
  345 |         pair(pair<_U1, _U2>&& __p)
      |         ^~~~
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:345:9: note:   template argument deduction/substitution failed:
<source>:9:30: note:   mismatched types 'std::pair<_T1, _T2>' and 'int'
    9 |     std::pair<V, V> p3{2, {2}};
      |                              ^
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:339:9: note: candidate: 'template<class _U1, class _U2>  requires  _S_constructible<const _U1&, const _U2&>() && _S_dangles<const _U1&, const _U2&>() constexpr std::pair<_T1, _T2>::pair(const std::pair<_U1, _U2>&) [with _U2 = _U1; _T1 = std::vector<int>; _T2 = std::vector<int>]' (deleted)
  339 |         pair(const pair<_U1, _U2>&) = delete;
      |         ^~~~
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:339:9: note:   template argument deduction/substitution failed:
<source>:9:30: note:   mismatched types 'const std::pair<_T1, _T2>' and 'int'
    9 |     std::pair<V, V> p3{2, {2}};
      |                              ^
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:330:9: note: candidate: 'template<class _U1, class _U2>  requires  _S_constructible<const _U1&, const _U2&>() && !_S_dangles<_U1, _U2>() constexpr std::pair<_T1, _T2>::pair(const std::pair<_U1, _U2>&) [with _U2 = _U1; _T1 = std::vector<int>; _T2 = std::vector<int>]'
  330 |         pair(const pair<_U1, _U2>& __p)
      |         ^~~~
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:330:9: note:   template argument deduction/substitution failed:
<source>:9:30: note:   mismatched types 'const std::pair<_T1, _T2>' and 'int'
    9 |     std::pair<V, V> p3{2, {2}};
      |                              ^
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:323:9: note: candidate: 'template<class _U1, class _U2>  requires  _S_constructible<_U1, _U2>() && _S_dangles<_U1, _U2>() constexpr std::pair<_T1, _T2>::pair(_U1&&, _U2&&) [with _U2 = _U1; _T1 = std::vector<int>; _T2 = std::vector<int>]' (deleted)
  323 |         pair(_U1&&, _U2&&) = delete;
      |         ^~~~
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:323:9: note:   template argument deduction/substitution failed:
<source>:9:30: note:   couldn't deduce template parameter '_U2'
    9 |     std::pair<V, V> p3{2, {2}};
      |                              ^
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:315:9: note: candidate: 'template<class _U1, class _U2>  requires  _S_constructible<_U1, _U2>() && !_S_dangles<_U1, _U2>() constexpr std::pair<_T1, _T2>::pair(_U1&&, _U2&&) [with _U2 = _U1; _T1 = std::vector<int>; _T2 = std::vector<int>]'
  315 |         pair(_U1&& __x, _U2&& __y)
      |         ^~~~
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:315:9: note:   template argument deduction/substitution failed:
<source>:9:30: note:   couldn't deduce template parameter '_U2'
    9 |     std::pair<V, V> p3{2, {2}};
      |                              ^
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:238:9: note: candidate: 'template<class ... _Args1, long unsigned int ..._Indexes1, class ... _Args2, long unsigned int ..._Indexes2> constexpr std::pair<_T1, _T2>::pair(std::tuple<_Args1 ...>&, std::tuple<_Args2 ...>&, std::_Index_tuple<_Indexes1 ...>, std::_Index_tuple<_Indexes2 ...>) [with _Args1 = {_Args1 ...}; long unsigned int ..._Indexes1 = {_Indexes1 ...}; _Args2 = {_Args2 ...}; long unsigned int ..._Indexes2 = {_Indexes2 ...}; _T1 = std::vector<int>; _T2 = std::vector<int>]'
  238 |         pair(tuple<_Args1...>&, tuple<_Args2...>&,
      |         ^~~~
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:238:9: note:   template argument deduction/substitution failed:
<source>:9:30: note:   mismatched types 'std::tuple<_UTypes ...>' and 'int'
    9 |     std::pair<V, V> p3{2, {2}};
      |                              ^
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:202:9: note: candidate: 'template<class ... _Args1, class ... _Args2> constexpr std::pair<_T1, _T2>::pair(std::piecewise_construct_t, std::tuple<_Args1 ...>, std::tuple<_Args2 ...>) [with _Args1 = {_Args1 ...}; _Args2 = {_Args2 ...}; _T1 = std::vector<int>; _T2 = std::vector<int>]'
  202 |         pair(piecewise_construct_t, tuple<_Args1...>, tuple<_Args2...>);
      |         ^~~~
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:202:9: note:   template argument deduction/substitution failed:
<source>:9:30: note:   candidate expects 3 arguments, 2 provided
    9 |     std::pair<V, V> p3{2, {2}};
      |                              ^
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:305:7: note: candidate: 'constexpr std::pair<_T1, _T2>::pair(const _T1&, const _T2&) requires  _S_constructible<const _T1&, const _T2&>() [with _T1 = std::vector<int>; _T2 = std::vector<int>]'
  305 |       pair(const _T1& __x, const _T2& __y)
      |       ^~~~
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:305:23: note:   no known conversion for argument 1 from 'int' to 'const std::vector<int>&'
  305 |       pair(const _T1& __x, const _T2& __y)
      |            ~~~~~~~~~~~^~~
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:249:7: note: candidate: 'constexpr std::pair<_T1, _T2>::pair() requires (is_default_constructible_v<_T1>) && (is_default_constructible_v<_T2>) [with _T1 = std::vector<int>; _T2 = std::vector<int>]'
  249 |       pair()
      |       ^~~~
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:249:7: note:   candidate expects 0 arguments, 2 provided
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:198:17: note: candidate: 'constexpr std::pair<_T1, _T2>::pair(std::pair<_T1, _T2>&&) [with _T1 = std::vector<int>; _T2 = std::vector<int>]'
  198 |       constexpr pair(pair&&) = default;         ///< Move constructor
      |                 ^~~~
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:198:17: note:   candidate expects 1 argument, 2 provided
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:197:17: note: candidate: 'constexpr std::pair<_T1, _T2>::pair(const std::pair<_T1, _T2>&) [with _T1 = std::vector<int>; _T2 = std::vector<int>]'
  197 |       constexpr pair(const pair&) = default;    ///< Copy constructor
      |                 ^~~~
/opt/compiler-explorer/gcc-trunk-20230315/include/c++/13.0.1/bits/stl_pair.h:197:17: note:   candidate expects 1 argument, 2 provided
Compiler returned: 1
Wilcox answered 15/3, 2023 at 14:54 Comment(10)
@JeJo I could, but that would also copy the vectors instead of building them in place; and it also used to work fine before. So the question stands: what changed?Wilcox
@Svalorzen: "I could, but that would also copy the vectors instead of building them in place" It's going to do that anyway. That is, std::pair<int, V> p2{1, {2}}; will not build the vector in-place. It will create a vector and move it into the pair.Uninstructed
@NicolBolas Really? Which one, the second vector or both? Looking at the C++ reference page, to me the only constructor that applies is the (3), which just forwards the arguments to the constructors (int and initializer list). Is that not what is happening?Wilcox
Same behavior change between clang 14 (compiles) ->15 (fails).Evie
Contrary behavior change between msvc 19.29 (fails) ->19.30 (compiles).Evie
@Svalorzen: "the only constructor that applies is the (3)" A braced-init-list ({}) can never be deduced by a template. It will not deduce to an initializer_list or anything else. If you use a {} in a function parameter list, that argument will never participate in template argument deduction. Therefore, it will select constructor 2, with t2 being initialized from the braced-init-list and then copied in.Uninstructed
@Svalorzen: Notice the two copies in this exampleUninstructed
P1008 might be related.Evie
@BenjaminBuch: Unlikely. Neither vector nor pair<int, vector<int>> are aggregates.Uninstructed
@NicolBolas That makes sense. So ideally I'd have to use piecewise_construct plus an explicit initializer_list as the second parameter, correct? If so, at least one of my questions is answered :)Wilcox
U
6

This looks like a bug fix (for C++20 and below). There is no properly-implemented version of C++ where this:

std::pair<V, V> p5{2, {2}};

Would compile.

A braced-init-list/initializer list (note the lack of an "_") is not an expression; it is its own separate grammatical construct. The way it participates in template argument deduction is really very simple.

It doesn't (mostly).

If the corresponding parameter is explicitly an initializer_list<E> of some sort, then it can deduce E. Otherwise it cannot and the parameter is non-deduced:

If removing references and cv-qualifiers from P gives std::initializer_list<P′> or P′[N] for some P′ and N and the argument is a non-empty initializer list ([dcl.init.list]), then deduction is performed instead for each element of the initializer list independently, taking P′ as separate function template parameter types P′i and the ith initializer element as the corresponding argument. In the P′[N] case, if N is a non-type template parameter, N is deduced from the length of the initializer list. Otherwise, an initializer list argument causes the parameter to be considered a non-deduced context ([temp.deduct.type]).

This is repeated:

A function parameter for which the associated argument is an initializer list ([dcl.init.list]) but the parameter does not have a type for which deduction from an initializer list is specified ([temp.deduct.call]).

And, of course, if a template parameter can't be deduced, then template argument deduction fails, and that function cannot be called.

If a template parameter is used only in non-deduced contexts and is not explicitly specified, template argument deduction fails.

This means that constructors like this:

template< class U1, class U2 >
pair( U1&& x, U2&& y );

Will not work on braced-init-lists. Therefore, the only viable constructors for {2, {2}} are ones where the second parameter is not a template parameter that needs deduction.

Which basically means this one:

pair( const T1& x, const T2& y );

T1 and T2 are from the class template parameters, not those of the function template. You already specified them as V and V, so that tries to be called.

However, vector<int> is not implicitly convertible from an integer 2. The constructor that takes a single integer is explicit, so attempting to initialize x will fail.

This constructor has been explicit since C++98. So std::pair<V, V> p5{2, {2}}; should never have worked. If it did, this was a bug in the implementation.


Note that C++23 changes one of pairs constructors to have default template parameters:

template< class U1 = T1, class U2 = T2 >
pair( U1&& x, U2&& y );

This would allow the {2, {2}} syntax to work, as it no longer relies on template argument deduction to get template parameter types.


Can it be compiled again without having to build the vectors and copying them in (i.e. not use V(2) somewhere)

This never worked. It always did a copy.

You could use piecewise_construct and initializer_list gymnastics to avoid a "copy". But really, just switch to using a tuple and use the type properly in the initializer list:

std::tuple<V, V> p5{V{2}, V{2}};

Or, to cut down on redundancy:

std::tuple p5{V{2}, V{2}};

This will move from the parameters, not copy from them.


However, I can't quite tell whether this answers why the two "symmetric" calls p(2, 2) and p({2},{2}) compile with no problems, and only break when there is no symmetry.

The latter works because it calls the T1, T2 constructor, which can implicitly convert from a braced-init-list into a vector.

The former works because the U1, U2 constructor will deduce these as two integers. And a vector is constructible (but not implicitly convertible) from an integers. So the two Vs can be constructed by direct initialization from those parameters.

The asymmetric one doesn't work because the presence of a braced-init-list shuts down the U1, U2 constructor entirely, since U2 cannot be deduced (again, until C++23).

Uninstructed answered 15/3, 2023 at 16:29 Comment(5)
I kind of understand this, although I'll have to read it multiple times just to make it settle. However, I can't quite tell whether this answers why the two "symmetric" calls p(2, 2) and p({2},{2}) compile with no problems, and only break when there is no symmetry.Wilcox
template<class U1 = T1, class U2 = T2> constexpr explicit(see below) pair(U1&& x, U2&& y); There are default template parameters here.Caribbean
MSVC 19.30 started to support it if and only if it is called with /std:c++latest ; godbolt.org/z/evT5131hd ; Is this a newly introduces bug in MSVC? GCC 12 and clang 15 both stopped to support this, while there predecessors supported it independently from the used standard.Evie
@康桓瑋: There are now in C++23, but not C++20 and before. I wasn't looking in the latest drafts, since the OP asked about C++20. I'll add it to the answer.Uninstructed
@Svalorzen: I've added the reasoning for why this is the case to the end of my answer.Uninstructed
C
-3

I don't know why exactly, but the compiler isn't able to figure out the type of {2} in p5. Telling it solves your issue.

#include <utility>
#include <vector>

using V = std::vector<int>;

int main() {
    std::pair<int, V> p1{1, 2};   // p1.second has 2 elements
    std::pair<int, V> p2{1, {2}}; // p2.second has 1 element

    std::pair<V, V> p3{2, 2};     // Both vectors have 2 elements
    std::pair<V, V> p4{{2}, {2}}; // Both vectors have 1 element

    std::pair<V, V> p5{2, V{2}};   // Does not compile
    // p5.first should have 2 elements, while the other should have 1
}
Courson answered 15/3, 2023 at 15:35 Comment(1)
The question is tagged language-lawyer.Wickner

© 2022 - 2024 — McMap. All rights reserved.