c++stddefault-constructorstdtuple

Why do I need to specify the type of a default constructed object in this situation?


I don't understand why in foobar below I need to specify std::vector<int>{} whereas in foobar2 I do not:

#include <iostream>
#include <memory>
#include <vector>
#include <tuple>

std::tuple<std::unique_ptr<int>, std::vector<int>> foobar() {
    std::unique_ptr<int> test = std::make_unique<int>(42);
    return { std::move(test), {} };    // <= this is a syntax error
    // return { std::move(test), std::vector<int>{} }  // <= this compiles...
}

std::tuple<int, std::vector<int>> foobar2() {
    return { {},  {} };
}

int main() {
    std::cout << *std::get<0>(foobar()) << "\n";
    std::cout << std::get<0>(foobar2()) << "\n";
    return 0;
}

The error message from GCC is

<source>: In function 'std::tuple<std::unique_ptr<int, std::default_delete<int> >, std::vector<int, std::allocator<int> > > foobar()':
<source>:8:34: error: could not convert '{std::move<unique_ptr<int>&>(test), <brace-enclosed initializer list>()}' from '<brace-enclosed initializer list>' to 'std::tuple<std::unique_ptr<int, std::default_delete<int> >, std::vector<int, std::allocator<int> > >'
    8 |     return { std::move(test), {} };    // <= this is a syntax error
      |                                  ^
      |                                  |
      |                                  <brace-enclosed initializer list>
Compiler returned: 1

Solution

  • Given a

    template< class... Types >
    class tuple;
    

    There are two possible constructors that can be used here. The first one is:

    tuple( const Types&... args );
    

    However it can only be used if all tuple members are copy-constructible. The unique_ptr is, of course, not copy-constructible.

    This leaves only one other possible constructor:

    template< class... UTypes >
    tuple( UTypes&&... args );
    

    That is, a forwarding constructor, a "Hail Mary" that forwards all its parameters to the constructor of each underlying tuple member.

    {}
    

    An empty braced-init list is typeless, and cannot be bound to a forwarding reference.

    This could possibly work if only there was one more constructor:

    tuple(Types && ... Args);
    

    that participates in overload resolution if all member types are movable. Alas, there isn't.

    whereas in foobar2 I do not:

    foobar2's tuple members are copy-constructible. The first constructor overload gets used.