Почему C++ не поддерживает сильно типизированный многоточие?
Может кто-нибудь объяснить мне, почему C++, по крайней мере, насколько мне известно, не реализует сильно типизированную функцию многоточия, что-то вроде:
void foo(double ...) {
// Do Something
}
Это означает, что, говоря простым языком: "Пользователь может передать переменное число терминов в функцию foo, однако все термины должны быть двойными"
8 ответов
Исторически синтаксис многоточия ...
происходит от C.
Этот сложный зверь был использован для власти printf
-подобные функции и должен использоваться с va_list
, va_start
так далее...
Как вы заметили, это небезопасно; но тогда C далек от того, чтобы быть безопасным, что с его неявными преобразованиями из и в void*
для любых типов указателей, его неявное усечение интегралов / значений с плавающей запятой и т. д.
Поскольку C++ должен был быть как можно ближе к надмножеству C, он унаследовал многоточие от C.
С самого начала практика C++ развивалась, и был сильный толчок к более строгой типизации.
В C++11 это завершилось:
- списки инициализаторов, сокращенный синтаксис для переменного числа значений данного типа:
foo({1, 2, 3, 4, 5})
- шаблоны variadic, которые являются собственным зверем и позволяют писать безопасные для типов
printf
например
Variadic шаблоны на самом деле повторно использовать многоточие ...
в их синтаксисе для обозначения пакетов типов или значений и в качестве оператора распаковки:
void print(std::ostream&) {}
template <typename T, typename... Args>
void print(std::ostream& out, T const& t, Args const&... args) {
print(out << t, args...); // recursive, unless there are no args left
// (in that case, it calls the first overload
// instead of recursing.)
}
Обратите внимание на 3 различных использования ...
:
typename...
объявить тип переменнойArgs const&...
объявить пакет аргументовargs...
распаковать пачку в выражении
Есть
void foo(std::initializer_list<double> values);
// foo( {1.5, 3.14, 2.7} );
что очень близко к этому.
Вы также можете использовать шаблоны с переменным числом аргументов, но это становится более дискурсивным. Что касается действительной причины, я бы сказал, что попытка ввести этот новый синтаксис, вероятно, не стоит того: как вы получаете доступ к отдельным элементам? Как вы знаете, когда остановиться? Что делает это лучше, чем, скажем, std::initializer_list
?
В C++ есть кое-что даже ближе к этому: нетиповые пакеты параметров.
template < non-type ... values>
как в
template <int ... Ints>
void foo()
{
for (int i : {Ints...} )
// do something with i
}
но у типа нетипичного параметра шаблона (эм) есть некоторые ограничения: он не может быть double
, например.
Это уже возможно с шаблонами variadic и SFINAE:
template <bool...> struct bool_pack;
template <bool... v>
using all_true = std::is_same<bool_pack<true, v...>, bool_pack<v..., true>>;
template <class... Doubles, class = std::enable_if_t<
all_true<std::is_convertible<Doubles, double>{}...>{}
>>
void foo(Doubles... args) {}
Спасибо Коломбо за хороший all_true
трюк. Вы также сможете использовать выражение сгиба в C++17.
Поскольку более поздние и будущие стандарты фокусируются на более кратком синтаксисе (краткие циклы for, неявные шаблоны функций...), вполне возможно, что предложенный вами синтаксис окажется в Стандарте однажды;)
Почему конкретно такая вещь не была предложена (или была предложена и отклонена), я не знаю. Такая вещь, безусловно, будет полезна, но добавит сложности языку. Как демонстрирует Квентин, уже существует C++11 способ достижения такой цели с помощью шаблонов.
Когда Concepts будет добавлен в стандарт, у нас будет другой, более краткий способ:
template <Convertible<double>... Args>
void foo(Args... doubles);
или же
template <typename... Args>
requires Convertible<Args, double>()...
void foo(Args... doubles);
или, как указывает @dyp:
void foo(Convertible<double>... doubles);
Лично, между текущим решением и теми, которые мы получим с помощью Концепций, я считаю, что это адекватное решение проблемы. Тем более, что последний - это то, о чем вы изначально просили.
Основываясь на ответе Мэтью:
void foo () {}
template <typename... Rest>
void foo (double arg, Rest... rest)
{
/* do something with arg */
foo(rest...);
}
Если код с помощью foo
компилирует, вы знаете, все аргументы могут быть преобразованы в double
,
template<typename T, typename... Arguments>
struct are_same;
template <typename T, typename A1, typename... Args>
struct are_same<T, A1, Args...>{ static const bool value = std::is_same<T, A1>::value && are_same<T, Args...>::value;};
template <typename T>
struct are_same<T>{static const bool value = true;};
template<typename T, typename... Arguments>
using requires_same = std::enable_if_t<are_same<T, Arguments...>::value>;
template <typename... Arguments, typename = requires_same<double, Arguments...>>
void foo(Arguments ... parameters)
{
}
Способ достижения (своего рода) того, что вы предлагаете, - это использование шаблонов с переменными параметрами.
template<typename... Arguments>
void foo(Arguments... parameters);
однако вы можете передать любой тип в пакете параметров сейчас. То, что вы предлагаете, никогда не было реализовано, может быть, это может быть отличным дополнением к языку, или это может быть слишком сложно реализовать, как есть. Вы всегда можете попробовать написать предложение и отправить его на isocpp.org.
Потому что вы можете использовать
void foo(std::vector<T> values);