Get advantages of an universal reference, without an universal reference
Asked Answered
G

6

15

Problem

Let's assume a function func that takes any container in the form Container<Type, N, Args...> (that is a container that takes as first template argument a type and as second an std::size_t defining how many arguments there are in the container) and returns its ith element if and only if N is between 40 and 42.

An example of such a container is std::array.

My first version of the function would be along the lines of:

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

and then I would need a const overload:

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

Question

Is it possible to define something like (this won't work because this is not an universal reference):

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

in order to define a single version of this function and make is work for both Container<Type, N, Args...>& and const Container<Type, N, Args...>&?

Guardianship answered 26/7, 2014 at 16:32 Comment(1)
If you have a better title, please, for the love of everything you believe in, feel free to replace the current one. It's terrible.Guardianship
I
7

You can't get the advantages of 'universal references' without actually using universal references, so just make Container be a 'universal reference' parameter. If you do this, all you need to do is use an alternative technique to find N.

One option is to simply make Container store N in a static variable (or in a typedef'd std::integral_constant or a constexpr function). The other option is to write a new (meta-)function whose sole purpose is to find N. I'd prefer the first option, but I'll write the second option in the answer, as it is less intrusive (it doesn't require any changes to 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];
}

Now you need the ability to forward container[i] with the cv-ness and value category of container. For this, use a helper function that is a generalization of std::forward. This is really ugly, since there isn't much support for this in the standard library (thankfully you only need to write this once ever, and it is useful in for quite a few different problems). First the type calculations:

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;

Now the function:

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

Now that the helper functions are defined, we can simply write func as:

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]);
}
Inotropic answered 26/7, 2014 at 17:49 Comment(2)
I know that this is a lot of code. If you don't need perfect forwarding of value category and cv-qualification, the code can be reduced a lot (as shown in the other answers). This answer has the advantage of disallowing dangerous conversions from rvalue containers to lvalue elements. Also, AFAIK, it isn't possible to take Container as a template-template parameter and get the behaviour that you want.Inotropic
When you say you want the category and type of container[i] maintained, do you basically mean a return type of decltype( ( container[i] ) )? (note the extra ( ) in that, which changes the behaviour of decltype in useful ways).Buckbuckaroo
P
6

I don't think you can get the advantage of the special deduction rules for universal references without using one. The workaround is somewhat straightforward - use a universal reference, and a trait class to match the template and extract 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];
}

Demo.

Poetess answered 26/7, 2014 at 17:9 Comment(0)
L
3

Try something like this:

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

Not really sure it was worth it.

Lemar answered 26/7, 2014 at 16:54 Comment(1)
Eh, just move it to the top one.Lemar
A
1

Whenever you see a question like this, think SFINAE. Then think "no, that is a bad idea, there must be another way". Usually that way involves tag dispatching.

Can we use tag dispatching? Yes, we can

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

and we have taken func, wrapped the type information into a types<?> tag, passed it to func_internal which extracts all of the tasty sub-type information from the types<?> tag, and the forward-state from the Container&&.

The body of your func simply migrates to func_internal, and if you get an error with the wrong type passed, the error will be types<blah> does not match types<Container<Type, N, Args...>>, which isn't a bad error.

You can also bundle multiple such matching into a single parameter.

Aldredge answered 26/7, 2014 at 18:58 Comment(0)
S
0

I believe the closest thing you can get in C++11 is something like this:

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

Example:

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{}); 
Sixtieth answered 26/7, 2014 at 17:54 Comment(0)
A
0

I can see only one solution:

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];
}
Antifouling answered 26/7, 2014 at 18:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.