Как выполнить идеальную пересылку, когда аргумент шаблона явно объявлен

Рассмотрим эту простую функцию

template <typename U>
auto mkVector(U&& x0)
{
    return std::vector<std::decay_t<U>>{std::forward<U>(x0)};
}

и 4 возможных варианта использования, когда аргументом является либо l-значение, либо r-значение, а тип либо указывается неявно, либо выводится из аргументов:

    const string lvalue("hello");

    // type inferred from arguments
    auto v1 = mkVector(lvalue);              // argument is a lvalue
    auto v2 = mkVector(string{});            // argument is a rvalue

    // type is explicilty stated
    auto v3 = mkVector<string>(lvalue);       // argument is a lvalue
    auto v4 = mkVector<string>("");           // argument is a rvalue

Случай v3 не компилируется, поскольку U явно объявлен как U=string, следовательно, U&& означает string&&, а lvalue несовместимо.

Есть ли способ написать функцию mkVector так, чтобы она корректно работала во всех возможных случаях, поддерживая идеальную переадресацию?

Лучшее, что я мог придумать, это написать две перегрузки функции, но это не идеально, и если есть N аргументов вместо 1, потребуется 2^N возможных перегрузок.

template <typename U, std::enable_if_t<std::is_rvalue_reference_v<U>, bool> = true>
auto mkVector(U&& x0)
{
    return std::vector<U>{std::move(x0)};
}

template <typename U>
auto mkVector(const U& x0)
{
    return std::vector<U>{x0};
}

Это зависит от того, чего вы хотите достичь. Каково предполагаемое значение mkVector<string> с явным аргументом шаблона?

Kerrek SB 17.05.2024 14:16

Существует семейство контейнеров шаблонов (например, my_array_t), определенных для небольшого набора типов (например, string). Я хочу иметь возможность писать makeContainer(value1, value2, ...), где все значения могут быть lvalues или values того же типа, который используется в контейнере, или могут быть приведены к типу контейнера. Например, контейнер определен для string, но не определен для const char*, и в этом случае мне придется явно указать тип контейнера: makeContainer<string>("hi", "hello").

Fabio 17.05.2024 14:35
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
106
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Есть ли способ написать функцию mkVector так, чтобы она корректно работала во всех возможных случаях, поддерживая идеальную переадресацию?

Нет, нет. Вы можете либо использовать ссылку для пересылки и по прихоти пользователя предоставить поддельный U, либо реализовать все функции std::forward внутри mkVector. Для этого вам понадобится как минимум две перегрузки, так что вы на правильном пути, но я бы не стал развивать эту идею дальше; это пустая трата времени.

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

template <typename T>
void f(T x) {
    use(std::move(x));
}

Если бы пользователь вызвал f<U&>(u) явно, то f переместился бы из заданного u, а не из локальной копии. Существует множество стандартных библиотечных функций, которые также не реализованы таким образом, чтобы обеспечить устойчивость к явным аргументам (например, std::swap). Это действительно основная проблема C++, которая гораздо более распространена, чем можно подумать.

Итак... что нам с этим делать? У вас есть два варианта:

  • Плевать. mkVector не следует использовать с явными аргументами, и виноват пользователь, если он использует его таким образом.
  • Не позволяйте пользователю явно передавать что-либо (полезное).

Для последнего подхода вы можете написать:

template <typename..., typename U>
auto mkVector(U&& x0)
{
    return std::vector<std::decay_t<U>>{std::forward<U>(x0)};
}

Любые явные аргументы, предоставленные mkVector, будут поглощены typename..., поэтому пользователь больше не сможет указать U явно. Технически это работает, но довольно необычно. Большинству людей просто все равно.

Спасибо за ваш ответ. Здесь я портирую на C+17 библиотеку кода объемом 100 миллионов строк, написанную на C++98. На данный момент у меня есть mkvector(const T1& a1), mkvector(const T1& a1, const T2& a2), ... до 20 аргументов. Явные аргументы шаблона необходимы в ситуации, когда контейнер имеет тип (например, string), а переданные аргументы имеют другой тип (например, const char *). Все четыре варианта использования, которые я описал, встречаются миллионы раз во всех вариантах. Если нет другого способа сделать это, я полагаю, мне придется отказаться от идеальной пересылки и определить аргументы const Ts&....

Fabio 17.05.2024 14:57

Кстати, классный трюк с использованием явных параметров typename... !

Fabio 17.05.2024 15:00

Да... Я не видел простого способа защитить эти функции от фиктивных действий, сохранив при этом место вызова. Вам просто нужно будет вставить typename..., чтобы отделить аргументы, которые необходимо предоставить явно, от выведенных, и обновить сайт вызова в случае неправильного использования. Хорошей новостью является то, что подавляющее большинство шаблонов функций не должны получать явных аргументов шаблона. Такие вещи, как std::forward и std::get, являются редкими исключениями, но обычно есть способ избежать этого.

Jan Schultke 17.05.2024 15:02

Возможно static_assert(!std::is_reference_v<T>); ... хотя я чувствую, что очень сложно сделать C++ надежным.

Eljay 17.05.2024 15:13

@Элджей, пример с f, да, это начало. Для mkVector пересылающая ссылка U&& означает, что U не является ссылкой или ссылкой lvalue. Единственный фиктивный случай, который не может произойти без явно предоставленных аргументов, — это выведение U по ссылке rvalue. Однако этого не должно произойти, если базой кода в настоящее время является C++98.

Jan Schultke 17.05.2024 15:25

@jan, возможно, решение состоит в том, чтобы использовать ваш typename..., а затем создать некоторые черты, которые преобразуют все возможные типы в небольшой набор типов, для которых существует мой контейнер. Например. traits<const char *>::type это string, traits<any integral type>::type это int и т. д.

Fabio 17.05.2024 15:43

Вы можете назвать этот начальный typename... и выполнить над ним некоторую метапрограммную обработку, если вам действительно нужны оба синтаксиса для работы. Хотя вряд ли это того стоит.

aschepler 17.05.2024 17:11
Ответ принят как подходящий

В идеале вам нужно что-то вроде этого:

template <typename T = std::decay_t<U>, typename U>
auto mkVector(U&& x0) {
    return std::vector<T>{std::forward<U>(x0)};
}

... Где T по умолчанию равен std::decay_t<U>, если не указано явно. Однако это не работает, поскольку по умолчанию нельзя ссылаться на более поздний аргумент.

Вы можете использовать тип «заполнителя», который, как вы знаете, не будет указан явно, например void, и заменить его значением по умолчанию, если это заполнитель.

template<typename T = void, typename U>
auto mkVector(U&& x0) {
    using type = std::conditional_t<std::is_void_v<T>, std::decay_t<U>, T>;
    return std::vector<type>{ std::forward<U>(x0) };
}

Я не знаю, что вы подразумеваете под «потребуется 2^N возможных перегрузок». Если бы у вас было больше аргументов, вы не могли бы явно указать второй аргумент, не указав явно первый, поэтому вам потребовалось бы максимум N+1 перегрузок. Для нескольких аргументов вы можете сделать что-то вроде этого:

namespace detail {
template<std::size_t N, typename Default, typename... Types>
auto nth_or_impl() {
    if constexpr (sizeof...(Types) > N) {
        return std::tuple_element<N, std::tuple<Types...>>{};
    } else {
        return std::enable_if<true, Default>{};
    }
}
}

template<std::size_t N, typename Default, typename... Types>
using nth_or = typename decltype(detail::nth_or_impl<N, Default, Types...>)_)::type;

template <typename... T, typename U0, typename U1, typename U2>
auto f(U0&& x0, U1&& u1, U2&& u2) {
    static_assert(sizeof...(T) <= 3, "f: Explicitly specified more than 3 template arguments");
    using T1 = nth_or<0, std::decay_t<U0>, T...>;
    using T2 = nth_or<1, std::decay_t<U1>, T...>;
    using T3 = nth_or<2, std::decay_t<U2>, T...>;
    // ...
}

Это также поддерживает пакеты параметров неизвестного размера.

template<class T, class U>
concept compatible = std::is_convertible_v<T, std::decay_t<U>> || std::is_void_v<U>;

template<class X, class Alt>
using fallback_t = std::conditional_t<std::is_void_v<X>, Alt, X>;

template <class T = void, compatible<T> U>
auto mkVector(U&& x0) {
  using V = fallback_t< T, std::decay_t<U> >;
  return std::vector<V>{std::forward<U>(x0)};
}

Это создает фабричную функцию mkVector. При желании можно передать тип T. Если ему передается тип T, он создает вектор этого типа и гарантирует, что ваш аргумент может быть сохранен в векторе этого типа.

Отсутствие совместимости приводит к ошибке разрешения перегрузки из-за несоответствия концепции.

В противном случае создается вектор std::decay_t<U>, где U — тип переданного значения.

Это легко масштабируется до большего количества аргументов, не требуя ручной реализации 2^n. Например

template<class K=void, class V=void, compatible<K>, Ku, compatible<V> Vu>
auto mkMap( Ku&& k, Vu&& v ) {
  using Km = fallback_t< K, std::decay_t<Ku> >;
  using Vm = fallback_t< V, std::decay_t<Vu> >;
  return std::map< Km, Vm >( { std::forward<Ku>(k), std::forward<Vu>(v) } );
}

с линейным кодом, а не с экспоненциальным кодом.

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