Search code examples
c++initializationlanguage-lawyerc++20std-pair

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


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

Solution

  • 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).