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

1

17

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
Hydrotropism answered 30/9, 2023 at 0:56 Comment(0)
B
14

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.

Berdichev answered 30/9, 2023 at 1:21 Comment(2)
They added default template arguments for one of the pair constructors in C++23, which solves problems like this. Unfortunately, you can't default a pack in current C++23, so this solution isn't applicable to tuples. (But I hear there is a proposal.)Theoretician
@BrianBi I actually noticed that the naked {} version works with a pair and wondered about that too.Hydrotropism

© 2022 - 2024 — McMap. All rights reserved.