SU3
SU3

Reputation: 5409

Brace initialize tuple in function argument

The main problem in question is that I want to write a function that takes two variable-length sets of arguments.

The abstraction I decided to go for is to emulate the following call syntax:

f({a,b,c},x,y);

If a, b, and c all have the same type, this can be made to work with

template <typename X, typename... A>
void f(std::initializer_list<X> xs, A&&... as) { . . . }

But a similar function definition with std::tuple instead of the std::initializer_list does not allow for my desired syntax.

template <typename... X, typename... A>
void f(const std::tuple<X...>& xs, A&&... as) { . . . }

Is there some trick I can use to allow the types in the first group to be heterogeneous?

Addendum: In general, I do not apriori know the size of X....

Addendum 2: Does anyone know the technical reason why a braced initializer works for std::tuple t{a,b,c}, but not in the template argument context? Is it because in my example xs is not expanded by X...?

Upvotes: 1

Views: 658

Answers (2)

user1095108
user1095108

Reputation: 14603

What you want is currently inconvenient, but there's another alternative to the one already provided:

f(a, b, c)(x)(y)();

If you exploit the fact, that f does not return anything, you can return a lambda or some other function object that accepts further arguments. An empty argument list could mean, that there will be no further arguments.

Example:

template <typename ...A>
auto f(A&& ...a)
{
  if constexpr (sizeof...(a) > 1)
  {
    std::tuple(std::forward<A>(a)...);
  }
  else
  {
     // burp
  }

  return [](auto&& ...a)
    {
      if constexpr (sizeof...(a))
      {
        return f(std::forward<decltype(a)>(a)...);
      }
      else
      {
        std::cout << "done" << std::endl;
      }
    };
}

int main()
{
  f(1, 2, 3)(4)(5)();

  return 0;
}

https://wandbox.org/permlink/eAB2S4aCxxiSlMAF

EDIT: Try also this alternative:

template <typename ...A>
void f(std::any const(&)[3], A&&...)
{
}

int main()
{
  f({1, 2u, '3'}, 4., 5.f);

  return 0;
}

https://wandbox.org/permlink/0OhoJpQ1YpSGOV2c

You can use a std::variant for type safety or something faster than what the STL provides. You can also write your own container, of course.

EDIT2: Then again:

template <typename ...A>
void f(std::initializer_list<std::any>, A&&...)
{
}

int main()
{
  f({1, 2u, '3'}, 4., 5.f);

  return 0;
}

Don't use std::any, of course, but your own fancy type.

https://wandbox.org/permlink/rqSVK6aj5zuSFRcf

EDIT3: There is yet another way:

class U
{
  void const* const v_;

  using typeid_t = void(*)();
  typeid_t const tid_;

public:
  template <typename T>
  static typeid_t type_id() noexcept
  {
    return typeid_t(U::type_id<T>);
  }

  template <typename A>
  U(A&& a) noexcept: v_(&a), tid_(type_id<std::remove_cvref_t<A>>()) {}

  template <typename A>
  A const& get() const noexcept { return *static_cast<A const*>(v_); }

  auto type_id() const noexcept { return tid_; }
};

template <typename... A>
void f(U const (&a)[3], A&&...)
{
  std::cout << a[0].get<double>() << std::endl;
}

int main()
{
  f({1.5, 2, 3.f}, 'a', 1);

  return 0;
}

You can make typeid checking optional and compile it only in the debug build.

https://wandbox.org/permlink/wgXOycB5I3bb3ExV

Upvotes: 1

walnut
walnut

Reputation: 22152

No, it is currently impossible with the syntax you want. The initializer list can only be deduced to an array or a std::initializer_list, both of homogeneous types.

This is a special rule. An initializer list has no type and simply cannot be deduced to anything else. If the function parameter is not an array or a std::initializer_list, then the parameter for which the initializer list was given becomes a non-deduced context and template argument deduction will ignore it.

In particular class template argument deduction cannot be done in a function parameter to determine the type from a braced initializer list as it can be done in a variable definition since C++17. Such a feature was not added to the language.

In principle you could allow a finite number of different types by using e.g. an array of std::variant as parameter, but that would be resolved to the type only at runtime and is unlikely to be what you want.

You need to either add a type name or function call to the initializer list braces as in

f(std::forward_as_tuple(a, b, c), x, y)

or you can use a tag type instead to indicate the end of the first pack in a flat argument list:

f(a, b, c, tag, x, y)

The tag can then be detected with some template work.

Upvotes: 1

Related Questions