4

I need to forward the values of a tuple to a member initializer:

struct Struct {
  Member1 member1;
  Member2 member2;

  template<typename Tuple1, typename Tuple2>
    Struct( Tuple1&& tuple1, Tuple2&& tuple2 )
      : member1(tuple1...), member2(tuple2...)
    {}
};

The code above obviously isn't valid. How can I express it?

Member1 and Member2 have no default/copy/move constructor.

I know about std::apply, as suggested in How do I expand a tuple into variadic template function's arguments?. I also know about std::make_from_tuple. But I wouldn't know how to use any of these in a member initializer.

Any C++ standard is fine (preferably C++17, but C++20 would work as well).

To clarify, my real goal is to create a Struct, passing it two sets of variadic arguments to perfect-forward them to initialize member1 and member2. I thought that "grouping" the two sets into tuples could have been a good idea, since that's what std::map::emplace does. Other approaches would work as well (e.g. passing a special object between the two sets of variadic arguments).

1201ProgramAlarm
  • 32,384
  • 7
  • 42
  • 56
Helloer
  • 417
  • 3
  • 13

2 Answers2

4

cppreference.com has a nice example of a sample implementation of std::make_from_tuple, however, as you've discovered you can't use it due to the lack of a copy constructor of the underlying class.

However, its breadcrumbs lets you adapt it to work around these limitations:

#include <tuple>
#include <iostream>

struct Member1 {
    Member1(int a, int b)
    {
        std::cout << a << " "
              << b
              << std::endl;
    }

    Member1(const Member1 &)=delete;
    Member1(Member1 &&)=delete;
};

struct Member2 {
    Member2(const char *str)
    {
        std::cout << str << std::endl;
    }

    Member2(const Member2 &)=delete;
    Member2(Member2 &&)=delete;
};

// De-obfucation shortcut

template<typename Tuple>
using make_index_sequence_helper=std::make_index_sequence
    <std::tuple_size_v<std::remove_reference_t<Tuple>>>;

struct Struct {
    Member1 member1;
    Member2 member2;

    template<typename Tuple1, typename Tuple2>
    Struct( Tuple1&& tuple1,
        Tuple2&& tuple2 )
        : Struct{std::forward<Tuple1>(tuple1),
        make_index_sequence_helper<Tuple1>{},
        std::forward<Tuple2>(tuple2),
        make_index_sequence_helper<Tuple2>{}}
    {
    }

    template<typename Tuple1, std::size_t ...tuple1_args,
         typename Tuple2, std::size_t ...tuple2_args>
    Struct(Tuple1 && tuple1,
           std::index_sequence<tuple1_args...>,
           Tuple2 && tuple2,
           std::index_sequence<tuple2_args...>)
        : member1{std::get<tuple1_args>(tuple1)...},
          member2{std::get<tuple2_args>(tuple2)...}
    {
    }
};

int main()
{
    Struct s{ std::tuple<int, int>{2, 3},
        std::tuple<const char *>{"Hello world"}};

    return 0;
}

Tested with gcc 10 with -std=c++17.

Sam Varshavchik
  • 114,536
  • 5
  • 94
  • 148
  • Ohh, thanks! I checked the cppreference multiple times, but couldn't grasp the index sequence trick. For some reason my brain decided that the example expanded the values to a helper function with something similar to `std::apply` and refused to understand the truth. – Helloer Dec 24 '20 at 23:30
  • `std::make_from_tuple` does not need copy nor move ctors due to copy elision, so it can be used just fine. – Quimby Dec 24 '20 at 23:31
  • I'm gonna accept Quimby's answer as it's simpler and it's the one I'll use. But I found this answer very useful and informative too! – Helloer Dec 24 '20 at 23:39
4

std::make_from_tuple is indeed the right choice:

#include <tuple>
struct Member1 {
    Member1(int x,float y, char z){}

    Member1(const Member1& other)=delete;
    Member1(Member1&& other)=delete;
};

struct Member2 {
    Member2(int x,float y, char z){}

    Member2(const Member2& other)=delete;
    Member2(Member2&& other)=delete;
};

struct Struct {
  Member1 member1;
  Member2 member2;

  template<typename Tuple1, typename Tuple2>
    Struct(Tuple1&& tuple1, Tuple2&& tuple2)
      : member1(std::make_from_tuple<Member1>(std::forward<Tuple1>(tuple1))),
       member2(std::make_from_tuple<Member2>(std::forward<Tuple2>(tuple2)))
    {}
};

int main(){
    Struct c(std::tuple{1,1.1,'c'},std::tuple{2,2.2,'x'});
}

Godbolt demo.

Quimby
  • 17,735
  • 4
  • 35
  • 55
  • Ohh, that looks very simple! I assumed that passing the result of `std::make_from_tuple` to an initializer would cause calling a copy constructor! I'm afraid I wrongly placed a `std::move` in there and that forced the compiler to use the deleted move constructor. – Helloer Dec 24 '20 at 23:37
  • By the way, the example doesn't build with GCC. It does work if I use `std::make_tuple` instead of initializing the `std::tuple` with an initializer list. I suspect it's a GCC bug. – Helloer Dec 24 '20 at 23:38
  • It would cause move constructor pre-C++17 without copy elision optimization as `std::make_from_tuple` returns rvalue. But after copy elision, there is no move or copy, the object is constructed in place. Yes, moving is actually bad because it disables the elision in this case. – Quimby Dec 24 '20 at 23:39
  • @Helloer That is weird, not sure what is causing that, template deductions for tuples should kick in here. – Quimby Dec 24 '20 at 23:44
  • @Helloer Can I ask a question about it or do you want to? I am interested about the cause of that error. – Quimby Dec 24 '20 at 23:51
  • @Helloer I updated the answer, the tuples should be forwarded, not moved, since they are universal references. – Quimby Dec 25 '20 at 00:13
  • Sorry, I was AFK until now. I see now. It makes sense. GCC error was cryptic. I was OK with using `std::make_tuple` too. – Helloer Dec 25 '20 at 01:55