Рассмотрим эту простую функцию
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};
}
Существует семейство контейнеров шаблонов (например, my_array_t
), определенных для небольшого набора типов (например, string
). Я хочу иметь возможность писать makeContainer(value1, value2, ...)
, где все значения могут быть lvalues
или values
того же типа, который используется в контейнере, или могут быть приведены к типу контейнера. Например, контейнер определен для string
, но не определен для const char*
, и в этом случае мне придется явно указать тип контейнера: makeContainer<string>("hi", "hello")
.
Есть ли способ написать функцию
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&...
.
Кстати, классный трюк с использованием явных параметров typename...
!
Да... Я не видел простого способа защитить эти функции от фиктивных действий, сохранив при этом место вызова. Вам просто нужно будет вставить typename...
, чтобы отделить аргументы, которые необходимо предоставить явно, от выведенных, и обновить сайт вызова в случае неправильного использования. Хорошей новостью является то, что подавляющее большинство шаблонов функций не должны получать явных аргументов шаблона. Такие вещи, как std::forward
и std::get
, являются редкими исключениями, но обычно есть способ избежать этого.
Возможно static_assert(!std::is_reference_v<T>);
... хотя я чувствую, что очень сложно сделать C++ надежным.
@Элджей, пример с f
, да, это начало. Для mkVector
пересылающая ссылка U&&
означает, что U
не является ссылкой или ссылкой lvalue. Единственный фиктивный случай, который не может произойти без явно предоставленных аргументов, — это выведение U
по ссылке rvalue. Однако этого не должно произойти, если базой кода в настоящее время является C++98.
@jan, возможно, решение состоит в том, чтобы использовать ваш typename...
, а затем создать некоторые черты, которые преобразуют все возможные типы в небольшой набор типов, для которых существует мой контейнер. Например. traits<const char *>::type
это string
, traits<any integral type>::type
это int
и т. д.
Вы можете назвать этот начальный typename...
и выполнить над ним некоторую метапрограммную обработку, если вам действительно нужны оба синтаксиса для работы. Хотя вряд ли это того стоит.
В идеале вам нужно что-то вроде этого:
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) } );
}
с линейным кодом, а не с экспоненциальным кодом.
Это зависит от того, чего вы хотите достичь. Каково предполагаемое значение
mkVector<string>
с явным аргументом шаблона?