Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get advantages of an universal reference, without an universal reference

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...>&?

like image 584
Shoe Avatar asked Jul 26 '14 16:07

Shoe


People also ask

What is universal reference?

Universal reference was a term Scott Meyers coined to describe the concept of taking an rvalue reference to a cv-unqualified template parameter, which can then be deduced as either a value or an lvalue reference.

What is reference collapsing?

Reference collapsing is the mechanism that leads to universal references (which are really just rvalue references in situations where reference-collapsing takes place) sometimes resolving to lvalue references and sometimes to rvalue references.

What does auto && mean in C++?

The auto && syntax uses two new features of C++11: The auto part lets the compiler deduce the type based on the context (the return value in this case). This is without any reference qualifications (allowing you to specify whether you want T , T & or T && for a deduced type T ). The && is the new move semantics.

What is forwarding reference in C++?

When t is a forwarding reference (a function argument that is declared as an rvalue reference to a cv-unqualified function template parameter), this overload forwards the argument to another function with the value category it had when passed to the calling function.


4 Answers

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]);
}
like image 186
Mankarse Avatar answered Oct 25 '22 17:10

Mankarse


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.

like image 20
T.C. Avatar answered Oct 25 '22 18:10

T.C.


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.

like image 28
Puppy Avatar answered Oct 25 '22 16:10

Puppy


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.

like image 30
Yakk - Adam Nevraumont Avatar answered Oct 25 '22 18:10

Yakk - Adam Nevraumont