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