Специализация шаблона функции C++ на основе шаблонного типа возвращаемого значения

В 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++.

Как насчет get_from_cin<std::vector<int>>(42);?

tkausl 13.07.2024 17:59

Я не думаю, что называть две функции по-разному - это не C++, это было бы разумным решением (пока вы можете предоставить красивые имена для функций). Создание возвращаемого типа в шаблоне (как предлагает комментарий выше) будет работать, но мне кажется это перебор. Или вместо этого вы можете использовать выходной параметр - они несколько не одобряются, но ИМХО это оправдано для такого варианта использования.

Yksisarvinen 13.07.2024 18:07

Вероятно, вы можете вернуть фиктивный тип и написать шаблонный автономный оператор присваивания, который определяет тип контейнера, но это кажется нелепым объемом работы для какого-то довольно сомнительного синтаксического сахара.

Useless 13.07.2024 18:34

Я не говорю, что это лучшее решение, но один из подходов, который использовали люди, — это возврат объекта, у которого есть оператор преобразования в vector или list.

chris 13.07.2024 19:13
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
4
112
6
Перейти к ответу Данный вопрос помечен как решенный

Ответы 6

Вы можете создать один шаблон функции, использовать 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выбрать значения fromcin»: она также считывает только фиксированное количество значений. , а затем дополнительно пытается заполнить этими значениями определенный контейнер.

В идеале вызывающий абонент должен:

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! Именно по этой причине я вместо этого использую итераторы.

ashpool 13.07.2024 20:50

Просчитанный итератор ввода действует мне на нервы. Я не могу перестать думать об этом. Если доступна альтернатива простому take, я воспользуюсь ею.

Red.Wave 13.07.2024 21:23

Вы можете получить именно тот синтаксис, который вам нужен, используя прокси:

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 в переменных и их передачу? Я хочу сохранить ссылку на входной поток внутри структуры, и поэтому она не должна существовать дольше, чем инструкция, в которой она была создана.

ashpool 21.07.2024 22:46

Другие вопросы по теме

Похожие вопросы

Вызов функции consteval внутри if consteval вызывает ошибку в контексте, отличном от constexpr
Можете ли вы вызвать статическую функцию-член constexpr во время компиляции?
Преобразование 32-битного COM-интерфейса в 64-битный
Проблемы с чтением файла с C++ в коде vs, не уверен, связано ли это с тем, что я работаю в Linux
C++17: явный первый параметр шаблона перед вариативным шаблоном
Преобразование ID2D1HwndRenderTarget Direct2D в ID2D1Bitmap
В GCC внутри лямбды я могу получить переменную constexpr из лямбды шаблона, отличного от constexpr, но не в Visual C++
ReadDirectoryChangesW: нет немедленного FILE_ACTION_MODIFIED при записи, ожидание закрытия или открытия дескриптора файла в файле
Получить переменную constexpr из лямбда-функции можно, но компиляция завершается неудачей (Visual C++) и штрафом (gcc), когда такой оператор находится в новой лямбда-функции
Ошибка сегментации при копировании пользовательского динамического массива