Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can one make a variadic-based chained-indexing function?

Tags:

c++

arrays

c++11

If I have an object a that either a built-in array or a class-type with a suitable operator [], and its returned type can be indexed itself, how should I write a generic function that can index all of them with a variadic call instead of separated bracket block? In other words, I can make expressions like:

a[i0][i1]...[iK]

and I want to be able to write it as a single function:

slice( a, i0, i1, ..., iK )

since the rules of C++ require operator [] to work on single arguments, making it less-compatible with variadic stuff. (This question is based on a Usenet thread where I tried asking something similar; ending up solving only possibly-nested built-in arrays myself.)

A first stab:

template < typename T, typename U >
constexpr
auto slice( T &&t, U &&u ) noexcept(???) -> ???
{ return ce_forward<T>(t)[ ce_forward<U>(u) ]; }

template < typename T, typename U, typename V, typename ...W >
constexpr
auto slice( T &&t, U &&u, V &&v, W... &&w ) noexcept(???) -> ???
{
    return slice( ce_forward<T>(t)[ce_forward<U>(u)], ce_forward<V>(v),
     ce_forward<W>(w)... );
}

The ce_forward function templates are a constexpr-marked std::forward. (Some things in the standard that could be marked constexpr aren't.) I'm trying to figure out the right stuff to put in the return type and exception specification spots. Some cases and caveats I know of are:

  • The built-in operator [] requires one operand to be a data-pointer (or decayed array reference) and the other to be an enumeration or integer type. The operands can be in either order. I don't know if an operand could be replaced with a class-type with an unambiguous (non-explicit?) conversion to an appropriate type.
  • A class type can define operator [] as non-static member function(s). Any such function can only have one parameter (besides this).
  • The built-in operator can't throw. (Unless operands could be based on an UDT conversion; said conversion is the only possible throw point.) The undefined behavior caused by boundary errors is beyond our purview here.
  • Whether the last indexing call returns a l-value reference, r-value reference, or value should be reflected in the return type of slice.
  • If the (final?) call is by-value, then std::is_nothrow_move_constructible<ReturnType>::value should be OR'd to the exception specification. (By-reference returns are noexcept.)
  • If the built-in operator involves an array, the returned reference for that step should be a r-value reference if the array is one too. (This is kind-of a defect because pointers, unlike arrays, lose their target's l- vs. r-value status, so the traditional return would always be a l-value reference.)
  • For class-type indexing operators, make sure the exception-spec and return-type sections refer to the same overload (if more than one) that the function body uses. Be wary for overloads that differ only in the qualification of this (const/volatile/both/neither and/or &/&&/neither).
like image 714
CTMacUser Avatar asked Oct 09 '22 02:10

CTMacUser


2 Answers

I've come up with a solution, based on the comment by @luc-danton and answer by @user315052.

#include <utility>

template < typename Base, typename ...Indices >
class indexing_result;

template < typename T >
class indexing_result<T>
{
public:
    using type = T;

    static constexpr
    bool  can_throw = false;
};

template < typename T, typename U, typename ...V >
class indexing_result<T, U, V...>
{
    using direct_type = decltype( std::declval<T>()[std::declval<U>()] );
    using   next_type = indexing_result<direct_type, V...>;

    static constexpr
    bool  direct_can_throw
     = not noexcept( std::declval<T>()[std::declval<U>()] );

public:
    using type = typename next_type::type;

    static constexpr
    bool  can_throw = direct_can_throw || next_type::can_throw;
};

template < typename T >
inline constexpr
auto  slice( T &&t ) noexcept -> T &&
{ return static_cast<T &&>(t); }

template < typename T, typename U, typename ...V >
inline constexpr
auto  slice( T &&t, U &&u, V &&...v )
  noexcept( !indexing_result<T, U, V...>::can_throw )
  -> typename indexing_result<T, U, V...>::type
{
    return slice( static_cast<T &&>(t)[static_cast<U &&>( u )],
     static_cast<V &&>(v)... );
}

I put a full example program up as a Gist. It worked on a web-site compiler running GCC >= 4.7, CLang >= 3.2, and Intel C++ >= 13.0.

like image 162
CTMacUser Avatar answered Oct 12 '22 10:10

CTMacUser


To get the right return value for the slice function, you can create a helper template that computes the correct result type. For example:

template <typename T, typename U, typename... V>
struct SliceResult {
    typedef typename SliceResult<T, V...>::Type Type;
};

template <typename T, typename U>
struct SliceResult<T, U> {
    typedef typename std::underlying_type<T[U(0)]>::type Type;
};

The slice function would then return a SliceResult<...>::Type.

like image 21
jxh Avatar answered Oct 12 '22 10:10

jxh