Forwarding reference behavior with definite types

191 views Asked by At

Suppose I have a template class

template <typename T> class foo;
template <typename... Args>
struct foo<std::tuple<Args...>> {
  std::tuple<Args...> t;
  foo(Args&&... args): t{std::forward<Args>(args)...} { }
};

I understand that in this case Args&&... are rvalue references, and I could have just as well written std::move instead of std::forward.

I can also have a constructor with lvalue references, like so

foo(const Args&... args): t{args...} { }

The question is whether it's possible to get the same behavior as with forwarding references, but for definite types? The reason I want this is so I can used syntax like

foo bar({. . .}, {. . .}, . . ., {. . .});

This works if I define the foo(Args&&... args) constructor, but doesn't allow for a mixed scenario, where I want to initialize some of the member tuple elements with brace-enclosed initializer lists and have others copied from preexisting object instances.

1

There are 1 answers

4
Yakk - Adam Nevraumont On BEST ANSWER

Sure; there is a fancy and simple way.

The fancy way I will detail below. First the simple way: take by value.

template <typename... Args>
struct foo<std::tuple<Args...>> {
  std::tuple<Args...> t;
  foo(Args... args): t{std::forward<Args>(args)...} { }
};

Really, just do this. Forward is used to do the right thing if Args contains a reference.

Taking by value adds one move over perfect forwarding, but reduces the requirement for overloads exponentially.


This is the fancy way. We type erase construction:

template<class T>
struct make_it {
  using maker=T(*)(void*);
  maker f;
  void* args;
  // make from move
  make_it( T&& t ):
    f([](void* pvoid)->T{
      return std::move(*static_cast<T*>(pvoid));
    }),
    args(std::addressof(t))
  {}
  // make from copy
  make_it( T const& t ):
    f([](void* pvoid)->T{
      return *(T const*)(pvoid);
    }),
    args(std::addressof(t))
  {}
  operator T()&&{return std::move(*this)();}
  T operator()()&&{ return f(args); }
};

This type erases construction by copy or move.

template <typename... Args>
struct foo<std::tuple<Args...>> {
  std::tuple<Args...> t;
  foo(make_it<Args>... args): t{std::move(args)()...} { }
};

It isn't perfectly transparent, but it is as close as I can get.

Double {{}} is required instead of single. It is a user defined conversion, so another one will not implicitly be done. We could add a universal ctor:'

  // make from universal
  template<class U>
  make_it( U&& u ):
    f([](void* pvoid)->T{
      return std::forward<U>(*(U*)(pvoid));
    }),
    args(std::addressof(u))
  {}

which works better if we add a sfinae teat that U&& can be used to implicitly construct T.

It has a few advantages, but they are marginal over just taking by value. For example, in C++17 non-movable types can be perfect forward constructed in some cases.