Search code examples
c++c++17stdtuple

std::tuple default constructor with move-constructable element


I'm trying to return a std::tuple containing an element that is a non-copy-constructable type. This seems to prevent me from using the default class constructor to construct the tuple. For example, to return a tuple containing Foo, a foo instance must be created and std::moved:

class Foo {
  public:
    Foo(const Foo&) = delete;
    Foo(Foo&&) = default;
    int x;
};

tuple<int, Foo> MakeFoo() {
    Foo foo{37};
//  return {42, {37}}; // error: could not convert ‘{42, {37}}’ from ‘’ to ‘std::tuple’
    return {42, std::move(foo)};
}

On the other hand, if the class is defined to have a copy constructor, construction of the tuple works fine:

class Bar {
  public:
    Bar(const Bar&) = default;
    int x;
};

tuple<int, Bar> MakeBar() {
    return {42, {37}}; // compiles ok
}

Is there a way to use the MakeBar syntax with the Foo class?


Solution

  • Not exactly, but you have (at least) two options.

    You could either spell out the Foo:

    tuple<int, Foo> MakeFoo() {
        return {42, Foo{37}}
    }
    

    Or you could add a constructor to Foo that replaces the aggregate initialization you're now doing:

    class Foo {
      public:
        Foo(const Foo&) = delete;
        Foo(Foo&&) = default;
        Foo(int x) : x(x) {}      // <--
        int x;
    };
    
    tuple<int, Foo> MakeFoo() {
        return {42, 37};
    }
    

    But why write return {42, 37} instead of return {42, {37}}? And why is adding a constructor needed to make this work?

    Constructing a tuple with at least one move-only type means we can't use the direct constructor

    tuple<Types...>::tuple(const Types &...)
    

    Instead, we have to use the templated converting constructor

    template<class... UTypes>
    tuple<Types...>::tuple(UTypes &&...)
    

    So the types of the two arguments will be deduced. But, {37} is an initializer list, which in this case makes the second function parameter a non-deduced context (see [temp.deduct.type]/5.6). So template argument deduction fails, and the constructor cannot be invoked. So, we have to write return {42, 37} instead to allow deduction to succeed.

    Additionally, this templated converting constructor only participates in overload resolution when the argument types are convertible to the respective tuple element types. And convertibility does not consider aggregate initialization, so an int is not convertible to the original Foo. But if we add the converting constructor Foo::Foo(int), int is now convertible to Foo, and the tuple constructor can be invoked.