В C++20 вместо выполнения
size_t count = 42;
std::vector<int> my_vector;
vec.reserve(count);
std::copy_n(std::istream_iterator<T>(std::cin), count, std::back_inserter(vec));
Я хочу иметь возможность просто сказать
std::vector<int> my_vector = get_from_cin<int>(42);
Проблема в том, что я также хочу иметь аналогичную возможность для std::list
:
std::list<int> my_list = get_from_cin<int>(42);
поэтому я попытался определить два шаблона функций:
template<typename T>
std::vector<T> get_from_cin(size_t count) {
std::vector<T> vec;
vec.reserve(count);
std::copy_n(std::istream_iterator<T>(std::cin), count, std::back_inserter(vec));
return vec;
}
template<typename T>
std::list<T> get_from_cin(size_t count) {
std::list<T> list;
std::copy_n(std::istream_iterator<T>(std::cin), count, std::back_inserter(list));
return list;
}
int main() {
std::vector<int> my_vector = get_from_cin<int>(42);
}
что (как и ожидалось) приводит к неоднозначности при специализации шаблона:
main.cpp:36:34: error: call to 'get_from_cin' is ambiguous
36 | std::vector<int> my_vector = get_from_cin<int>(42);
| ^~~~~~~~~~~~~~~~~
...
Ссылка на Godbolt: https://godbolt.org/z/EbdxTEcns
Как мне изменить код, чтобы эта неоднозначность была устранена и результирующий код вызова шаблонной функции был максимально кратким?
ПС. Я бы просто назвал эти две функции по-другому, но это противоречит стилю C++.
Я не думаю, что называть две функции по-разному - это не C++, это было бы разумным решением (пока вы можете предоставить красивые имена для функций). Создание возвращаемого типа в шаблоне (как предлагает комментарий выше) будет работать, но мне кажется это перебор. Или вместо этого вы можете использовать выходной параметр - они несколько не одобряются, но ИМХО это оправдано для такого варианта использования.
Вероятно, вы можете вернуть фиктивный тип и написать шаблонный автономный оператор присваивания, который определяет тип контейнера, но это кажется нелепым объемом работы для какого-то довольно сомнительного синтаксического сахара.
Я не говорю, что это лучшее решение, но один из подходов, который использовали люди, — это возврат объекта, у которого есть оператор преобразования в vector
или list
.
Вы можете создать один шаблон функции, использовать constexpr if
и передать аргумент шаблона шаблона, как показано ниже:
template<template<typename...>typename C, typename T>
auto get_from_cin(size_t count) {
C<T> vec;
if constexpr(std::is_same_v<std::vector<T>, C<T>>)
{
vec.reserve(count);
}
std::copy_n(std::istream_iterator<T>(std::cin), count, std::back_inserter(vec));
return vec;
}
int main() {
auto my_vector = get_from_cin<std::vector, int>(15);
auto my_list = get_from_cin<std::list, int>(15);
}
Вы не можете перегружать/специализироваться на основе типа возвращаемого значения.
Кроме того, как принято в ответе в этом посте, объясняется, что есть преимущества, позволяющие избегать использования специализации шаблонов, когда возможна перегрузка.
Однако вы можете перегрузить шаблоны функций (один для vector<T>
, другой для list<T>
и т. д.) на основе выходного параметра, передаваемого по ссылке:
#include <vector>
#include <list>
#include <iterator>
#include <iostream>
#include <algorithm>
template<typename T>
void get_from_cin(size_t count, std::vector<T> & vec) {
vec.reserve(count);
std::copy_n(std::istream_iterator<T>(std::cin), count, std::back_inserter(vec));
}
template<typename T>
void get_from_cin(size_t count, std::list<T> & list) {
std::copy_n(std::istream_iterator<T>(std::cin), count, std::back_inserter(list));
}
int main() {
std::vector<int> my_vector;
get_from_cin(42, my_vector);
std::list<int> my_list;
get_from_cin(42, my_list);
}
Трудность, с которой сталкивается функция get_from_cin
, и более важная, чем техническая сложность невозможности перегрузки по типу возвращаемого значения, заключается в том, что она пытается сделать больше, чем просто «get
выбрать значения from
cin
»: она также считывает только фиксированное количество значений. , а затем дополнительно пытается заполнить этими значениями определенный контейнер.
В идеале вызывающий абонент должен:
auto vec = std::views::istream<int>(std::cin) // read ints from cin
| std::views::take(42) // but only 42
| std::ranges::to<std::vector>(); // and put them into a vector, or list, or whatever
поскольку такой способ написания кода позволяет более четко выразить логику.*
Конечно, вы все равно можете поместить первые два этапа конвейера в функцию, параметризованную подсчетом целых чисел, если это часто повторяющийся шаблон, и предоставить вызывающему объекту самому решать, какой контейнер заполнить.
Для приведенного выше фрагмента требуется C++23 (для std::ranges::to
), но грубую реализацию можно написать** так:
template<template<typename...> typename Container>
auto To(std::ranges::input_range auto&& range) {
Container<std::ranges::range_value_t<decltype(range)>> result;
std::ranges::copy(range, std::back_inserter(result));
return result;
}
и тогда вызов выглядит так:
auto vec = To<std::vector>(std::views::istream<int>(std::cin)
| std::views::take(42));
* Является ли этот синтаксис более читабельным, это субъективно и в некоторой степени зависит от вкусов и предпочтений ваших соавторов в отношении «хорошего C++».
** Правильная реализация To
потребует больше работы и учета крайних случаев, которые я здесь полностью проигнорировал. Кроме того, вероятно, хотелось бы предоставить соответствующий operator|
, чтобы To
можно было использовать в конвейере более естественно.
Вот демо.
Если вы умеете использовать C++23, то может помочь что-то вроде этого:
#include <ranges>
#include <cassert>
auto vec = std::views::istream<int>(cin)
| std::views::take(42)
| std::ranges::to<std::vector>();
assert((size(vec) == 42));
Однако недостатком является то, что если пользователь вместо этого введет 43 числа, 43-е число будет удалено навсегда. Особый случай подсчитываемого входного итератора является нерешенной проблемой в рамках стандартного стандарта. За надежным решением по-прежнему следует reserve
, а затем copy_n
. Я не одобряю синтаксис template<template>
в пользовательском коде, поэтому не иду по этому пути. Но что-то вроде этого может быть полезно:
template<std::ranges::range C>
requires (not std::ranges::view<C>)
auto from_istream(std::istream& ins, std::size_t n, auto&& ...args)
requires std::constructible_from<C, decltype(args)...>
{
C res{std::forward<decltype(args)>(args)...};
if constexpr (
requires (C cnt, std::size_t sz)
{ cnt.reserve(sz); }
) res.reserve(n + res.size());
std::copy_n
( std::istream_iterator
< std::ranges::range_value_t<C> >(ins)
, n, std::back_inserter(res));
return res;//nrvo copy elision.
};
Теперь вы можете создать его экземпляр следующим образом:
auto vec = from_istream<std::vector<int>>(cin,42);
assert((vec.size == 42));
auto lst = from_istream<std::list<int>>(cin,42);
В любом случае вам понадобится идентификатор типа вашего объекта в правой части задания, чтобы указать компилятору, что создавать. Затем вывод типа и прямая инициализация обрабатывают все остальное.
Спасибо за упоминание проблемы views::istream | views::take
! Именно по этой причине я вместо этого использую итераторы.
Просчитанный итератор ввода действует мне на нервы. Я не могу перестать думать об этом. Если доступна альтернатива простому take
, я воспользуюсь ею.
Вы можете получить именно тот синтаксис, который вам нужен, используя прокси:
struct GetFromCinProxy
{
std::size_t count;
GetFromCinProxy(std::size_t count)
: count{count}
{}
template <class T>
operator std::vector<T>() const
{
std::vector<T> vec;
vec.reserve(count);
std::copy_n(std::istream_iterator<T>(std::cin), count, std::back_inserter(vec));
return vec;
}
template <class T>
operator std::list<T>() const
{
std::list<T> list;
std::copy_n(std::istream_iterator<T>(std::cin), count, std::back_inserter(list));
return list;
}
};
GetFromCinProxy get_from_cin(std::size_t count)
{
return {count};
};
Использование именно так, как вы хотите:
int main()
{
std::vector<int> v = get_from_cin(24);
std::list<int> l = get_from_cin(24);
}
Однако есть некоторые недостатки, самым большим из которых является неожиданное поведение при использовании выведенного типа:
auto x = get_from_cin(24);
Пользователи могут рассчитывать на получение итерируемого контейнера в x
. Кроме того, входные данные здесь не используются, что может быть достаточно нелогичным и вызывать ошибки.
Вы можете сделать что-то подобное, чтобы получить тот же эффект: создать одну функцию с двумя операторами преобразования.
struct get_from_cin_return {
std::size_t count;
template<typename T>
operator std::vector<T>() && {
// ...
}
template<typename T>
operator std::list<T>() && {
// ...
}
};
auto get_from_cin(std::size_t count) {
return get_from_cin_return{ count };
}
std::vector<int> i = get_from_cin(20);
std::list<double> d = get_from_cin(10);
В этой версии также отсутствует параметр шаблона в get_from_cin<T>
, но вы можете добавить его обратно, если хотите быть более явным. Вы также можете вернуться к своим «разным функциям для разных типов» с еще более явным get_from_cin<std::vector<T>>
/get_from_cin<std::list<T>>
. Это могло бы быть более понятно написано как get_from_cin(n) | std::ranges::to<vector_or_list>
Это действительно интересно! Есть ли способ предотвратить сохранение объектов get_from_cin_return
в переменных и их передачу? Я хочу сохранить ссылку на входной поток внутри структуры, и поэтому она не должна существовать дольше, чем инструкция, в которой она была создана.
Как насчет
get_from_cin<std::vector<int>>(42);
?