Получите преимущества универсальной ссылки без универсальной ссылки

Проблема

Предположим, что есть функция func, которая принимает любой контейнер в форме Container<Type, N, Args...> (то есть контейнер, который принимает в качестве первого аргумента шаблона тип, а в качестве второго — std::size_t, определяющий количество аргументов в контейнере) и возвращает свой ith элемент тогда и только тогда, когда 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];
}

чтобы определить единую версию этой функции и сделать так, чтобы она работала как для Container<Type, N, Args...>&, так и для const Container<Type, N, Args...>&?


person Shoe    schedule 26.07.2014    source источник
comment
Если у вас есть лучшее название, пожалуйста, ради всего, во что вы верите, не стесняйтесь заменить текущее. Это ужасно.   -  person Shoe    schedule 26.07.2014


Ответы (6)


Вы не можете получить преимущества «универсальных ссылок» без фактического использования универсальных ссылок, поэтому просто сделайте Container параметром «универсальной ссылки». Если вы это сделаете, все, что вам нужно сделать, это использовать альтернативный метод, чтобы найти N.

Один из вариантов — просто заставить Container хранить N в переменной static (или в функции typedef 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]);
}
person Mankarse    schedule 26.07.2014
comment
Я знаю, что это много кода. Если вам не нужна идеальная пересылка категории значений и cv-квалификации, код можно значительно сократить (как показано в других ответах). Преимущество этого ответа в том, что он запрещает опасные преобразования из rvalue контейнеров в lvalue элементов. Кроме того, насколько я знаю, невозможно взять Container в качестве параметра шаблона-шаблона и получить желаемое поведение. - person Mankarse; 26.07.2014
comment
Когда вы говорите, что хотите сохранить категорию и тип container[i], вы в основном имеете в виду возвращаемый тип decltype( ( container[i] ) )? (обратите внимание на дополнительный ( ), который изменяет поведение decltype полезным образом). - person Aaron McDaid; 08.08.2015

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

Демо.

person T.C.    schedule 26.07.2014

Попробуйте что-то вроде этого:

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);
}

Не совсем уверен, что оно того стоило.

person Puppy    schedule 26.07.2014
comment
Эх, просто переместите его наверх. - person Puppy; 26.07.2014

Всякий раз, когда вы видите подобный вопрос, подумайте о 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...>>, что не является плохой ошибкой.

Вы также можете объединить несколько таких совпадений в один параметр.

person Yakk - Adam Nevraumont    schedule 26.07.2014

Я считаю, что самое близкое, что вы можете получить в С++ 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{}); 
person sbabbi    schedule 26.07.2014

Я вижу только одно решение:

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];
}
person Kirilodius    schedule 26.07.2014