Получите преимущества универсальной ссылки без универсальной ссылки
Проблема
Предположим, что функция func
принимает любой контейнер в форме Container<Type, N, Args...>
(то есть контейнер, который принимает в качестве первого аргумента шаблона тип, а второй - std::size_t
, определяющий, сколько аргументов присутствует в контейнере) и возвращает его i
-й элемент тогда и только тогда, когда N
находится между 40
и 42
.
Примером такого контейнера является std::array
.
Моя первая версия функции будет выглядеть так:
template
< template<class, std::size_t, class...> class Container
, class Type
, std::size_t N
, class... Args >
auto func(std::size_t i, Container<Type, N, Args...>& container) -> decltype(container[0]) {
static_assert(N >= 40 && N <= 42, "bla bla bla");
return container[i];
}
а затем мне понадобится перегрузка const
:
template
< template<class, std::size_t, class...> class Container
, class Type
, std::size_t N
, class... Args >
auto func(std::size_t i, const Container<Type, N, Args...>& container) -> decltype(container[0]) {
static_assert(N >= 40 && N <= 42, "bla bla bla");
return container[i];
}
Вопрос
Можно ли определить что-то вроде этого (это не сработает, потому что это не универсальная ссылка):
template
< template<class, std::size_t, class...> class Container
, class Type
, std::size_t N
, class... Args >
auto func(std::size_t i, Container<Type, N, Args...>&& container) -> decltype(container[0]) {
// ^^
static_assert(N >= 40 && N <= 42, "bla bla bla");
return container[i];
}
чтобы определить одну версию этой функции, и make работает как для Container<Type, N, Args...>&
, так и const Container<Type, N, Args...>&
?
Ответы
Ответ 1
Вы не можете получить преимущества "универсальных ссылок" без фактического использования универсальных ссылок, поэтому просто сделайте Container
"параметром универсальной ссылки". Если вы сделаете это, все, что вам нужно сделать, это использовать альтернативный метод для поиска N
.
Один из вариантов состоит в том, чтобы просто сделать Container
хранить N
в переменной static
(или в функции typedef
'd std::integral_constant
или constexpr
). Другой вариант - написать новую (мета) функцию, единственной целью которой является поиск N
. Я бы предпочел первый вариант, но я напишу второй вариант в ответе, поскольку он менее навязчив (он не требует никаких изменений в Container
).
//This can alternatively be written as a trait struct.
template
< template<class, std::size_t, class...> class Container
, class Type
, std::size_t N
, class... Args >
constexpr std::size_t get_N(Container<Type, N, Args...> const&) { return N; }
template <class Container>
auto func(std::size_t i, Container &&container) -> decltype(container[i]) {
//alternatively, use std::tuple_size or container.size() or whatever
constexpr std::size_t N = get_N(container);
static_assert(N >= 40 && N <= 42, "bla bla bla");
return container[i];
}
Теперь вам нужно переслать container[i]
с категорией cv-ness и value Container
. Для этого используйте вспомогательную функцию, которая является обобщением std::forward
. Это очень уродливо, так как в стандартной библиотеке это не так много поддержки (к счастью вам только нужно написать это когда-нибудь, и это полезно для целого ряда различных проблем). Сначала вычисляются типы:
template<typename Prototype, typename T_value, typename T_decayed>
using forward_Const_t =
typename std::conditional<
std::is_const<Prototype>::value || std::is_const<T_value>::value,
T_decayed const,
T_decayed
>::type;
template<typename Prototype, typename T_value, typename T_decayed>
using forward_CV_t =
typename std::conditional<
std::is_volatile<Prototype>::value || std::is_volatile<T_value>::value,
forward_Const_t<Prototype, T_value, T_decayed> volatile,
forward_Const_t<Prototype, T_value, T_decayed>
>::type;
template<typename Prototype, typename T>
struct forward_asT {
static_assert(
std::is_reference<Prototype>::value,
"When forwarding, we only want to be casting, not creating new objects.");
static_assert(
!(std::is_lvalue_reference<Prototype>::value &&
std::is_rvalue_reference<T>::value),
"Casting an rvalue into an lvalue reference is dangerous");
typedef typename std::remove_reference<Prototype>::type Prototype_value_t;
typedef typename std::decay<T>::type T_decayed;
typedef typename std::remove_reference<T>::type T_value;
typedef typename std::conditional<
std::is_lvalue_reference<Prototype>::value,
forward_CV_t<Prototype_value_t, T_value, T_decayed> &,
forward_CV_t<Prototype_value_t, T_value, T_decayed> &&>::type type;
};
template<typename Prototype, typename T>
using forward_asT_t = typename forward_asT<Prototype,T>::type;
Теперь функция:
//Forwards `val` with the cv qualification and value category of `Prototype`
template<typename Prototype, typename T>
constexpr auto forward_as(T &&val) -> forward_asT_t<Prototype, T &&> {
return static_cast<forward_asT_t<Prototype, T &&>>(val);
}
Теперь, когда вспомогательные функции определены, мы можем просто написать func
как:
template <typename Container>
auto func(std::size_t i, Container &&container) ->
decltype(forward_as<Container &&>(container[i]))
{
constexpr std::size_t N = get_N(container);
static_assert(N >= 40 && N <= 42, "bla bla bla");
return forward_as<Container &&>(container[i]);
}
Ответ 2
Я не думаю, что вы можете воспользоваться преимуществами специальных правил дедукции для универсальных ссылок, не используя их. Обходной путь несколько прост - используйте универсальную ссылку и класс признаков для соответствия шаблону и извлеките N
:
template<class T> struct matched : std::false_type { };
template< template<class, std::size_t, class...> class Container
, class Type
, std::size_t N
, class... Args >
struct matched<Container<Type, N, Args...>> : std::true_type {
constexpr static std::size_t size = N;
};
template
< class Container, typename=std::enable_if_t<matched<std::decay_t<Container>>::value> >
auto func(std::size_t i, Container&& container) -> decltype(container[0]) {
static_assert(matched<std::decay_t<Container>>::size >= 40 && matched<std::decay_t<Container>>::size <= 42, "bla bla bla");
return container[i];
}
Демо.
Ответ 3
Попробуйте что-то вроде этого:
template<typename U, typename T> struct F;
template<template<class, std::size_t, class...> class Container
, class Type
, std::size_t N
, typename T
, class... Args> struct F<Container<Type, N, Args...>, T> {
static auto func(std::size_t i, T&& t) {
static_assert(N >= 40 && N <= 42, "bla bla bla");
return t[i];
}
}
template<typename U> auto func(std::size_t i, U&& container) {
return F<std::decay<U>::type, U>::func(i, container);
}
Не уверен, что это того стоило.
Ответ 4
Всякий раз, когда вы видите такой вопрос, подумайте SFINAE. Тогда подумайте "нет, это плохая идея, должен быть другой путь". Обычно этот способ включает отправку тегов.
Можно ли использовать диспетчеризацию тегов? Да, мы можем
template<class...> struct types{using type=types;};
// in case your C++ library lacks it:
template<class T>using decay_t=typename std::decay<T>::type;
template
< template<class, std::size_t, class...> class Container
, class Type
, std::size_t N
, class... Args
, class Container
>
auto func_internal(
std::size_t i,
types<Container<Type, N, Args...>>,
Container&& container
) -> decltype(container[0]) {
static_assert(N >= 40 && N <= 42, "bla bla bla");
return container[i];
}
template<class Container>
auto func( std::size_t i, Container&& container )
-> func_internal( i, types<decay_t<Container>>{}, std::declval<Container>() )
{
return func_internal( i, types<decay_t<Container>>{}, std::forward<Container>(container) );
}
и мы взяли func
, обернули информацию о типе в тег types<?>
, передали его в func_internal
, который извлекает всю вкусную информацию подтипа из тега types<?>
, а состояние вперед из Container&&
.
Тело вашего func
просто переносится на func_internal
, и если вы получите ошибку с неправильным переданным типом, ошибка будет types<blah>
не соответствует types<Container<Type, N, Args...>>
, что не является плохой ошибкой.
Вы также можете объединить несколько таких совпадений в один параметр.
Ответ 5
Я считаю, что ближайшая вещь, которую вы можете получить в С++ 11, примерно такая:
template<class Container>
struct container_traits{};
template<
template<class, std::size_t, class...> class Container,
class Type,
std::size_t N,
class... Args>
struct container_traits< Container<Type, N, Args ... > >
{
typedef Type type;
enum {size = N};
};
template<class Container,
unsigned N = container_traits<
typename std::remove_reference<Container>::type >::size>
auto func(std::size_t i, Container && container) -> decltype(container[0])
{
static_assert(N >= 40 && N <= 42, "bla bla bla");
return container[i];
}
Пример:
std::array<int,41> a;
func(41,a); // Ok, pased by lvalue ref.
// "static assert, bla bla bla" (would be passed by rvalue-ref)
func(1, std::array<int,2>{});
// Error, no suitable overload overload, the only func available fails with SFINAE
func(15, int{});
Ответ 6
Я вижу только одно решение:
template<
template<typename , std::size_t, class...> class ContainerType
, typename Type
, std::size_t N
, class... Args
>
void test(const ContainerType<Type, N, Args...>&){
static_assert(N >= 40 && N <= 42, "bla bla bla");
}
template
<typename ContainerType> // you need to be less specific in your container type declaration here to allow compiler deduce const ContainerType&& and ContainerType&& for you
auto func(std::size_t i, ContainerType&& container) -> decltype(container[0]) {
test(container); // compiler will throw it out because only static check is here.
return container[i];
}