15

Consider this function template:

template <class... T>
void foo (std::tuple<T, char, double> ... x);

This invocation works:

using K = std::tuple<int, char, double>;
foo ( K{1,'2',3.0}, K{4,'5',6.0}, K{7,'8',9.0} );

This one doesn't:

foo ( {1,'2',3.0}, {4,'5',6.0}, {7,'8',9.0} );

(gcc and clang both complain about too many arguments for foo)

Why is the second call a problem? Can I rewrite the declaration of foo so that the second call is also accepted?

Thee template parameter T is only used to implement variadicity. The actual type is known and fixed, only the number of arguments varies. In real life the types are different from int, char, double, this is just an example.

I cannot use C++17 for this. A C++11-compatible solution is much preferred.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
  • Is the first argument ever going to be something other than `int`? Probably; you want it to deduce the type from the first argument of the initializer list, right? – Rakete1111 Dec 09 '18 at 11:39
  • @Rakete1111 This is actually unlikely. All the types are known beforehand, only the number of arguments varies. I will be happy with a solution that fixes all the types. (But it cannot be a C-style variadic function). Added this info to the question. – n. m. could be an AI Dec 09 '18 at 11:53
  • 2
    [this](http://coliru.stacked-crooked.com/a/3dc6ec971a83d4cd), [this](http://coliru.stacked-crooked.com/a/3821592f77dd3c62) or [this](http://coliru.stacked-crooked.com/a/061b0d3b9cc96386) – Piotr Skotnicki Dec 09 '18 at 12:45
  • 1
    @PiotrSkotnicki That looks promising for the OP's requirement(C++11). Then why you don't paste them as an answer? (*Just curious*) – JeJo Dec 09 '18 at 12:52
  • @PiotrSkotnicki unfortunately 1 doesn't work for me. I have updated the question to reflect the problem more precisely. 2 is no good, there isn't such thing as an empty integer, 0 is just as `int` as any other. 3 looks promising though I don't quite understand it. – n. m. could be an AI Dec 09 '18 at 13:00
  • 1
    With different types in it, `{1, '2', 3.0}` can't be deduced as `std::initializer_list` or C-style array; and can't be deduced as `std::tuple` because `{1, `2`, 3.0}` itself isn't a `std::tuple`. I suppose you have to use `K`, or explicit the type calling `foo()` (so `foo( {1,'2',3.0}, {4,'5',6.0}, {7,'8',9.0} );`) or avoid the braces, at least for the first triplet (so `foo(1,'2',3.0, {4,'5',6.0}, {7,'8',9.0})`) to permit `T` deduction. – max66 Dec 09 '18 at 13:13
  • 1
    @n.m. then what's wrong with providing a sufficient number of overloads like [here](http://coliru.stacked-crooked.com/a/66e73d7dcad12c2b) ? – Piotr Skotnicki Dec 09 '18 at 13:20
  • 1
    My third hypothesis before require and additional couple of braces: `foo(1,'2',3.0, {{4,'5',6.0}, {7,'8',9.0}})`. So the first `1` is deduced as `int` and the following triplets as a `std::tupleconst [2]` – max66 Dec 09 '18 at 13:20
  • 1
    @max66 OP is more concerned about passing a variable number of arguments, and being able to know how many of them were actually provided, not deducing the first type. and your `foo(...)` idea would actually need to be `foo(...)` – Piotr Skotnicki Dec 09 '18 at 13:26
  • @PiotrSkotnicki - uhmm... as far I undestand, also the deduction of the first type is part of the problem; but you're right regarding my `foo()` idea: an additional couple of braces is required. – max66 Dec 09 '18 at 13:37
  • @PiotrSkotnicki Many overloads is a cumbersome workaround, I'd rather use an alias or additional braces. – n. m. could be an AI Dec 09 '18 at 13:38
  • @n.m. you could use Boost.PP to generate those overloads, and have a single implementation as in my last snippet – Piotr Skotnicki Dec 09 '18 at 13:39
  • as pointed by Piotr, my explicit-first-type idea (`foo( {1,'2',3.0}, {4,'5',6.0}, {7,'8',9.0} );`) is wrong: also in this case, an additional couple of braces is required (so `foo({{1,'2',3.0}, {4,'5',6.0}, {7,'8',9.0}});`) so you get a `std::tuple const (&arr)[N]` (where `T` is explicated and `N` is deduced). – max66 Dec 09 '18 at 13:40

3 Answers3

9

Generate an overloaded set of constructors:

#include <tuple>
#include <cstddef>

template <typename T, std::size_t M>
using indexed = T;

template <typename T, std::size_t M, std::size_t... Is>
struct initializer : initializer<T, M, sizeof...(Is) + 1, Is...>
{    
    using initializer<T, M, sizeof...(Is) + 1, Is...>::initializer;

    initializer(indexed<T, Is>... ts)
    {
        // ts is a pack of std::tuple<int, char, double>
    }
};

template <typename T, std::size_t M, std::size_t... Is>
struct initializer<T, M, M, Is...> {};

using foo = initializer<std::tuple<int, char, double>, 20>;
//                                   tuples limit+1 ~~~^

int main()
{
    foo({1,'2',3.0});
    foo({1,'2',3.0}, {4,'5',6.0});
    foo({1,'2',3.0}, {4,'5',6.0}, {7,'8',9.0});
}

DEMO


Generate an overloaded set of function call operators:

#include <tuple>
#include <cstddef>

template <typename T, std::size_t M>
using indexed = T;

template <typename T, std::size_t M, std::size_t... Is>
struct initializer : initializer<T, M, sizeof...(Is) + 1, Is...>
{    
    using initializer<T, M, sizeof...(Is) + 1, Is...>::operator();

    int operator()(indexed<T, Is>... ts) const
    {            
        // ts is a pack of std::tuple<int, char, double>
        return 1;
    }
};

template <typename T, std::size_t M, std::size_t... Is>
struct initializer<T, M, M, Is...>
{
    int operator()() const { return 0; }
};

static constexpr initializer<std::tuple<int, char, double>, 20> foo = {};
//                                        tuples limit+1 ~~~^

int main()
{    
    foo({1,'2',3.0});
    foo({1,'2',3.0}, {4,'5',6.0});
    foo({1,'2',3.0}, {4,'5',6.0}, {7,'8',9.0});
}

DEMO 2


Create (or generate with preprocessor macros) a set of overloads that forward arguments to a single implementation:

#include <array>
#include <tuple>

using K = std::tuple<int, char, double>;

void foo(const std::array<K*, 5>& a)
{
    // a is an array of at most 5 non-null std::tuple<int, char, double>*
}

void foo(K p0) { foo({&p0}); }
void foo(K p0, K p1) { foo({&p0, &p1}); }
void foo(K p0, K p1, K p2) { foo({&p0, &p1, &p2}); }
void foo(K p0, K p1, K p2, K p3) { foo({&p0, &p1, &p2, &p3}); }
void foo(K p0, K p1, K p2, K p3, K p4) { foo({&p0, &p1, &p2, &p3, &p4}); }

int main()
{
    foo({1,'2',3.0});
    foo({1,'2',3.0}, {4,'5',6.0});
    foo({1,'2',3.0}, {4,'5',6.0}, {7,'8',9.0});
}

DEMO 3


Pass as an array and deduce its size (requires additional pair of parens):

#include <tuple>
#include <cstddef>

template <std::size_t N>
void foo(const std::tuple<int, char, double> (&a)[N])
{
    // a is an array of exactly N std::tuple<int, char, double>
}

int main()
{
    foo({{1,'2',3.0}, {4,'5',6.0}});
 //     ^~~~~~ extra parens ~~~~~^
}

DEMO 4


Use an std::initializer_list as a constructor parameter (to skip extra parens):

#include <tuple>
#include <initializer_list>

struct foo
{
    foo(std::initializer_list<std::tuple<int, char, double>> li)
    {
        // li is an initializer list of std::tuple<int, char, double>
    }
};

int main()
{
    foo{ {1,'2',3.0}, {4,'5',6.0} };
}

DEMO 5

Piotr Skotnicki
  • 46,953
  • 7
  • 118
  • 160
4

{} is not an expression hence don't have type, argument deduction is concerned about types, special care is taken when the argument used to perform argument deduction is an initializer list the template function parameter must have specifics forms, otherwise the parameter is a non-deduced context. A more simplistic example is this:

template <class T> struct A { T r; };
template <class T>
void foo (A<T> x);

using K = A<int>;
foo({1}); // fail
foo(K{1}); // compile

This is covered by [temp.deduc.call]/1

If removing references and cv-qualifiers from P gives std::initializer_­list<P'> or P'[N] for some P' and N and the argument is a non-empty initializer list ([dcl.init.list]), then deduction is performed instead for each element of the initializer list, taking P' as a function template parameter type and the initializer element as its argument, and in the P'[N] case, if N is a non-type template parameter, N is deduced from the length of the initializer list. Otherwise, an initializer list argument causes the parameter to be considered a non-deduced context

and [temp.deduct.type]/5

The non-deduced contexts are:

(5.6) A function parameter for which the associated argument is an initializer list ([dcl.init.list]) but the parameter does not have a type for which deduction from an initializer list is specified ([temp.deduct.call]).

When you:

  • explicitly provide template arguments, it works ... nothing to deduce
  • specify the argument as K{1}, it works ... the argument is not longer an initializer list, is an expression with type.
Community
  • 1
  • 1
Jans
  • 11,064
  • 3
  • 37
  • 45
0

I cannot use C++17 for this. A C++11-compatible solution is much preferred.

With C++11 is a little more complicated (no std::index_sequence, no std::make_index_sequence) but, if you want maintain the variadic use of tuples... that is... if you substantially want something as

foo (std::tuple<int, char, double> ... ts)

and if you accept to call a static method of a template struct, you can define a template struct that recursively inherit itself and, recursively, define a

func ();
func (K t0);
func (K t0, K t1);
func (K t0, K t1, K t2);

where K is your

using K = std::tuple<int, char, double>;

The following is a full compiling C++11 example

#include <tuple>
#include <iostream>

using K = std::tuple<int, char, double>;

template <typename T, std::size_t>
struct getTypeStruct
 { using type = T; };

template <typename T, std::size_t N>
using getType = typename getTypeStruct<T, N>::type;

template <int ...>
struct iList;

template <std::size_t = 50u, std::size_t = 0u, typename = iList<>>
struct foo;

template <std::size_t Top, std::size_t N, int ... Is>
struct foo<Top, N, iList<Is...>> : public foo<Top, N+1u, iList<0, Is...>>
 {
   using foo<Top, N+1u, iList<0, Is...>>::func;

   static void func (getType<K, Is> ... ts)
    { std::cout << sizeof...(ts) << std::endl; }
 };

template <std::size_t Top, int ... Is>
struct foo<Top, Top, iList<Is...>>
 {
   // fake func, for recursion ground case
   static void func ()
    { }
 };


int main()
 {
   foo<>::func({1,'2',3.0}); // print 1
   foo<>::func({1,'2',3.0}, {4,'5',6.0}); // print 2
   foo<>::func({1,'2',3.0}, {4,'5',6.0}, {7,'8',9.0});  // print 3
 }

If you can use C++14, you can use std::make_index_sequence and std::index_sequence and the code become a little better, IMHO

#include <tuple>
#include <iostream>
#include <type_traits>

using K = std::tuple<int, char, double>;

template <std::size_t ... Is>
constexpr auto getIndexSequence (std::index_sequence<Is...> is)
   -> decltype(is);

template <std::size_t N>
using IndSeqFrom = decltype(getIndexSequence(std::make_index_sequence<N>{}));

template <typename T, std::size_t>
struct getTypeStruct
 { using type = T; };

template <typename T, std::size_t N>
using getType = typename getTypeStruct<T, N>::type;

template <std::size_t N = 50, typename = IndSeqFrom<N>>
struct foo;

template <std::size_t N, std::size_t ... Is>
struct foo<N, std::index_sequence<Is...>> : public foo<N-1u>
 {
   using foo<N-1u>::func;

   static void func (getType<K, Is> ... ts)
    { std::cout << sizeof...(ts) << std::endl; }
 };

template <>
struct foo<0, std::index_sequence<>>
 {
   static void func ()
    { std::cout << "0" << std::endl; }
 };

int main()
 {
   foo<>::func({1,'2',3.0});  // print 1
   foo<>::func({1,'2',3.0}, {4,'5',6.0});  // print 2
   foo<>::func({1,'2',3.0}, {4,'5',6.0}, {7,'8',9.0});  // print 3
 }

It's a pity you can't use C++17 because in you could use variadic unsing and avoid at all recursive inheritance

#include <tuple>
#include <iostream>
#include <type_traits>

using K = std::tuple<int, char, double>;

template <std::size_t ... Is>
constexpr auto getIndexSequence (std::index_sequence<Is...> is)
   -> decltype(is);

template <std::size_t N>
using IndSeqFrom = decltype(getIndexSequence(std::make_index_sequence<N>{}));

template <typename T, std::size_t>
struct getTypeStruct
 { using type = T; };

template <typename T, std::size_t N>
using getType = typename getTypeStruct<T, N>::type;

template <std::size_t N, typename = IndSeqFrom<N>>
struct bar;

template <std::size_t N, std::size_t ... Is>
struct bar<N, std::index_sequence<Is...>>
 {
   static void func (getType<K, Is> ... ts)
    { std::cout << sizeof...(ts) << std::endl; }
 };

template <std::size_t N = 50, typename = IndSeqFrom<N>>
struct foo;

template <std::size_t N, std::size_t ... Is>
struct foo<N, std::index_sequence<Is...>> : public bar<Is>...
 { using bar<Is>::func...; };

int main()
 {
   foo<>::func({1,'2',3.0});  // print 1
   foo<>::func({1,'2',3.0}, {4,'5',6.0});  // print 2
   foo<>::func({1,'2',3.0}, {4,'5',6.0}, {7,'8',9.0});  // print 3
 }
max66
  • 65,235
  • 10
  • 71
  • 111