Проблема с вызовом шаблона функции с переменным числом аргументов при передаче аргументов списка инициализатора скобок

Рассмотрим этот шаблон функции:

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

Этот вызов работает:

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

Этот не:

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

(gcc и clang жалуются на слишком много аргументов для foo)

Почему второй звонок - проблема? Могу ли я переписать объявление foo так, чтобы второй вызов также был принят?

Параметр шаблона T используется только для реализации вариативности. Фактический тип известен и фиксирован, меняется только количество аргументов. В реальной жизни типы отличаются от int, char, double, это просто пример.

Я не могу использовать C++17 для этого. Гораздо предпочтительнее решение, совместимое с C++11.


person n. 1.8e9-where's-my-share m.    schedule 09.12.2018    source источник
comment
Будет ли первый аргумент чем-то отличным от int? Вероятно; вы хотите, чтобы он выводил тип из первого аргумента списка инициализаторов, верно?   -  person Rakete1111    schedule 09.12.2018
comment
@ Rakete1111 На самом деле это маловероятно. Все типы известны заранее, меняется только количество аргументов. Я буду рад решению, которое исправляет все типы. (Но это не может быть вариационная функция в стиле C). Добавил эту информацию в вопрос.   -  person n. 1.8e9-where's-my-share m.    schedule 09.12.2018
comment
это, это или это   -  person Piotr Skotnicki    schedule 09.12.2018
comment
@PiotrSkotnicki Это выглядит многообещающе для требований OP (C ++ 11). Тогда почему вы не вставляете их в качестве ответа? (Просто любопытно)   -  person JeJo    schedule 09.12.2018
comment
@PiotrSkotnicki, к сожалению, 1 у меня не работает. Я обновил вопрос, чтобы более точно отразить проблему. 2 не годится, пустого целого числа не бывает, 0 такое же int, как и любое другое. 3 выглядит многообещающе, хотя я не совсем понимаю.   -  person n. 1.8e9-where's-my-share m.    schedule 09.12.2018
comment
Имея в нем разные типы, {1, '2', 3.0} нельзя вывести как std::initializer_list или массив в стиле C; и не может быть выведено как std::tuple<T, char, double>, потому что _4std::initializer_list5_ само по себе не является std::tuple. Я предполагаю, что вам нужно использовать K или явно указать тип, вызывающий foo() (так что foo<int>( {1,'2',3.0}, {4,'5',6.0}, {7,'8',9.0} );), или избегать фигурных скобок, по крайней мере, для первого триплета (так foo(1,'2',3.0, {4,'5',6.0}, {7,'8',9.0})), чтобы разрешить вывод T.   -  person max66    schedule 09.12.2018
comment
@н.м. тогда что плохого в предоставлении достаточного количества перегрузок, таких как здесь?   -  person Piotr Skotnicki    schedule 09.12.2018
comment
Моя третья гипотеза перед требованием и дополнительной парой фигурных скобок: foo(1,'2',3.0, {{4,'5',6.0}, {7,'8',9.0}}). Таким образом, первая 1 выводится как int, а следующие триплеты - как std::tuple<int, char, double>const [2].   -  person max66    schedule 09.12.2018
comment
@ max66 OP больше заботится о передаче переменного количества аргументов и возможности узнать, сколько из них было фактически предоставлено, а не о выводе первого типа. и ваша foo<int>(...) идея на самом деле должна быть foo<int, int, int>(...)   -  person Piotr Skotnicki    schedule 09.12.2018
comment
@PiotrSkotnicki - ммм ... насколько я понимаю, вывод первого типа также является частью проблемы; но вы правы относительно моей foo<int>() идеи: требуется дополнительная пара фигурных скобок.   -  person max66    schedule 09.12.2018
comment
@PiotrSkotnicki Многие перегрузки - громоздкий обходной путь, я бы предпочел использовать псевдоним или дополнительные фигурные скобки.   -  person n. 1.8e9-where's-my-share m.    schedule 09.12.2018
comment
@н.м. вы можете использовать Boost.PP для создания этих перегрузок и иметь единую реализацию, как в моем последнем фрагменте.   -  person Piotr Skotnicki    schedule 09.12.2018
comment
как указал Петр, моя идея явного первого типа (foo<int>( {1,'2',3.0}, {4,'5',6.0}, {7,'8',9.0} );) неверна: также в этом случае требуется дополнительная пара фигурных скобок (так что foo<int>({{1,'2',3.0}, {4,'5',6.0}, {7,'8',9.0}});), поэтому вы получаете std::tuple<T, char, double> const (&arr)[N] (где T разъясняется и N выводится).   -  person max66    schedule 09.12.2018


Ответы (3)


Создайте перегруженный набор конструкторов:

#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});
}

ДЕМО


Сгенерируйте перегруженный набор операторов вызова функций:

#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});
}

ДЕМО 2


Создайте (или сгенерируйте с помощью макросов препроцессора) набор перегрузок, которые передают аргументы в одну реализацию:

#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});
}

ДЕМО 3


Передайте как массив и определите его размер (требуется дополнительная пара скобок):

#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 ~~~~~^
}

ДЕМО 4


Используйте std::initializer_list в качестве параметра конструктора (чтобы пропустить лишние скобки):

#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} };
}

ДЕМО 5

person Piotr Skotnicki    schedule 09.12.2018
comment
интересно первое решение - person max66; 09.12.2018

{} не является выражением, поэтому не имеет типа, вывод аргумента связан с типами, особое внимание уделяется, когда аргумент, используемый для выполнения вывода аргумента, является списком инициализаторов, параметр функции шаблона должен иметь конкретные формы , в противном случае параметр является невыведенным контекстом. Более упрощенный пример:

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

Это описано в [temp.deduc.call]/1.

Если удаление ссылок и cv-квалификаторов из P дает std::initializer_­list<P'> или P'[N] для некоторых P' и N, а аргумент является непустым списком инициализаторов ([dcl.init.list]), то вместо этого выполняется вычитание для каждого элемента списка инициализаторов. , принимая P' в качестве типа параметра шаблона функции и элемент инициализатора в качестве его аргумента, а в случае P'[N], если N не является параметром шаблона, не относящимся к типу, N выводится из длины списка инициализаторов. В противном случае аргумент списка инициализаторов приводит к тому, что параметр считается невыведенным контекстом.

и [temp.deduct.type]/5

Невыведенные контексты:

(5.6) Параметр функции, для которого ассоциированный аргумент является списком инициализаторов ([dcl.init.list]), но параметр не имеет типа, для которого указан вывод из списка инициализаторов ([temp.deduct.call]). .

Когда ты:

  • явно указать аргументы шаблона, это работает... нечего делать
  • укажите аргумент как K{1}, он работает... аргумент больше не является списком инициализаторов, это выражение с типом.
person Jans    schedule 09.12.2018

Я не могу использовать C++17 для этого. Гораздо предпочтительнее решение, совместимое с C++11.

С С++ 11 немного сложнее (без std::index_sequence, без std::make_index_sequence), но, если вы хотите поддерживать вариативное использование кортежей... то есть... если вы существенно хотите что-то вроде

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

и если вы принимаете вызов статического метода структуры шаблона, вы можете определить структуру шаблона, которая рекурсивно наследует себя и, рекурсивно, определяет

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

где K твой

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

Ниже приведен пример полной компиляции C++11.

#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
 }

Если вы можете использовать C++14, вы можете использовать std::make_index_sequence и std::index_sequence, и код станет немного лучше, ИМХО

#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
 }

Жаль, что вы не можете использовать С++ 17, потому что вы могли бы использовать вариативный unsing и вообще избежать рекурсивного наследования

#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
 }
person max66    schedule 09.12.2018