Problem with calling a variadic function template when passing brace initialiser list arguments
Asked Answered
U

3

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.

Unfortunate answered 9/12, 2018 at 11:29 Comment(13)
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?Magnetometer
@Magnetometer 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.Unfortunate
this, this or thisToilet
@PiotrSkotnicki That looks promising for the OP's requirement(C++11). Then why you don't paste them as an answer? (Just curious)Cusk
@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.Unfortunate
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<T, char, double> 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<int>( {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.Sunwise
@n.m. then what's wrong with providing a sufficient number of overloads like here ?Toilet
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::tuple<int, char, double>const [2]Sunwise
@Sunwise 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<int>(...) idea would actually need to be foo<int, int, int>(...)Toilet
@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<int>() idea: an additional couple of braces is required.Sunwise
@PiotrSkotnicki Many overloads is a cumbersome workaround, I'd rather use an alias or additional braces.Unfortunate
@n.m. you could use Boost.PP to generate those overloads, and have a single implementation as in my last snippetToilet
as pointed by Piotr, my explicit-first-type idea (foo<int>( {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<int>({{1,'2',3.0}, {4,'5',6.0}, {7,'8',9.0}});) so you get a std::tuple<T, char, double> const (&arr)[N] (where T is explicated and N is deduced).Sunwise
T
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

Toilet answered 9/12, 2018 at 14:14 Comment(1)
intriguing the first one solutionSunwise
C
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.
Cluster answered 9/12, 2018 at 13:40 Comment(0)
S
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
 }
Sunwise answered 9/12, 2018 at 16:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.