¿Cómo puedo obtener la profundidad de un std :: vector multidimensional en tiempo de compilación?

45

Tengo una función que toma una std::vectordimensión multidimensional y requiere que se pase la profundidad (o el número de dimensiones) como parámetro de plantilla. En lugar de codificar este valor, me gustaría escribir una constexprfunción que tome std::vectory devuelva la profundidad como unsigned integervalor.

Por ejemplo:

std::vector<std::vector<std::vector<int>>> v =
{
    { { 0, 1}, { 2, 3 } },
    { { 4, 5}, { 6, 7 } },
};

// Returns 3
size_t depth = GetDepth(v);

Sin embargo, esto debe hacerse en tiempo de compilación porque esta profundidad se pasará a la función de plantilla como un parámetro de plantilla:

// Same as calling foo<3>(v);
foo<GetDepth(v)>(v);

¿Hay alguna forma de hacer esto?

tjwrona1992
fuente
44
El tamaño de a std::vectores algo en tiempo de ejecución, no en tiempo de compilación. Si desea un contenedor de tamaño de tiempo de compilación, busque std::array. También; recuerde que constexprsolo significa " puede ser evaluado en tiempo de compilación" - no hay promesa de que lo será . Se puede evaluar en tiempo de ejecución.
Jesper Juhl
55
@JesperJuhl, no busco tamaño, busco profundidad. Dos cosas muy diferentes. Quiero saber cuántos std::vectors están anidados entre sí. Por ejemplo std::vector<std::vector<int>> v;, con , GetDepth(v);devolvería 2 ya que es un vector bidimensional. El tamaño es irrelevante.
tjwrona1992
44
Semi-relacionado: anidado vectorno siempre es la mejor manera de hacer las cosas. La indexación manual en 2D o 3D de un solo vector plano puede ser más eficiente, dependiendo del caso de uso. (Solo matemáticas enteras en lugar de perseguir punteros desde los niveles exteriores.)
Peter Cordes
1
@PeterCordes Una mejor eficiencia es solo una faceta. Otra es que un tipo plano representa mejor la naturaleza contigua de la matriz. Una estructura anidada (de longitudes individuales potencialmente diferentes) es fundamentalmente un desajuste de tipo para representar un hiperrectangulo contiguo n-dimensional.
Konrad Rudolph
44
En cuanto a la nomenclatura, la biblioteca estándar utiliza rankpara esta consulta en tipos de matriz (de acuerdo con la nomenclatura matemática para tensores). Quizás esa sea una palabra mejor aquí que "profundidad".
dmckee --- ex-gatito moderador

Respuestas:

48

Un clásico problema de plantillas. Aquí hay una solución simple como la biblioteca estándar de C ++. La idea básica es tener una plantilla recursiva que contará una por una cada dimensión, con un caso base de 0 para cualquier tipo que no sea un vector.

#include <vector>
#include <type_traits>

template<typename T>
struct dimensions : std::integral_constant<std::size_t, 0> {};

template<typename T>
struct dimensions<std::vector<T>> : std::integral_constant<std::size_t, 1 + dimensions<T>::value> {};

template<typename T>
inline constexpr std::size_t dimensions_v = dimensions<T>::value; // (C++17)

Entonces puedes usarlo así:

dimensions<vector<vector<vector<int>>>>::value; // 3
// OR
dimensions_v<vector<vector<vector<int>>>>; // also 3 (C++17)

Editar:

Ok, he terminado la implementación general para cualquier tipo de contenedor. Tenga en cuenta que definí un tipo de contenedor como cualquier cosa que tenga un tipo de iterador bien formado según la expresión begin(t)donde std::beginse importa para la búsqueda de ADL y tes un valor de tipo l T.

Aquí está mi código junto con comentarios para explicar por qué funcionan las cosas y los casos de prueba que utilicé. Tenga en cuenta que esto requiere C ++ 17 para compilar.

#include <iostream>
#include <vector>
#include <array>
#include <type_traits>

using std::begin; // import std::begin for handling C-style array with the same ADL idiom as the other types

// decide whether T is a container type - i define this as anything that has a well formed begin iterator type.
// we return true/false to determing if T is a container type.
// we use the type conversion ability of nullptr to std::nullptr_t or void* (prefers std::nullptr_t overload if it exists).
// use SFINAE to conditionally enable the std::nullptr_t overload.
// these types might not have a default constructor, so return a pointer to it.
// base case returns void* which we decay to void to represent not a container.
template<typename T>
void *_iter_elem(void*) { return nullptr; }
template<typename T>
typename std::iterator_traits<decltype(begin(*(T*)nullptr))>::value_type *_iter_elem(std::nullptr_t) { return nullptr; }

// this is just a convenience wrapper to make the above user friendly
template<typename T>
struct container_stuff
{
    typedef std::remove_pointer_t<decltype(_iter_elem<T>(nullptr))> elem_t;    // the element type if T is a container, otherwise void
    static inline constexpr bool is_container = !std::is_same_v<elem_t, void>; // true iff T is a container
};

// and our old dimension counting logic (now uses std:nullptr_t SFINAE logic)
template<typename T>
constexpr std::size_t _dimensions(void*) { return 0; }

template<typename T, std::enable_if_t<container_stuff<T>::is_container, int> = 0>
constexpr std::size_t _dimensions(std::nullptr_t) { return 1 + _dimensions<typename container_stuff<T>::elem_t>(nullptr); }

// and our nice little alias
template<typename T>
inline constexpr std::size_t dimensions_v = _dimensions<T>(nullptr);

int main()
{
    std::cout << container_stuff<int>::is_container << '\n';                 // false
    std::cout << container_stuff<int[6]>::is_container<< '\n';               // true
    std::cout << container_stuff<std::vector<int>>::is_container << '\n';    // true
    std::cout << container_stuff<std::array<int, 3>>::is_container << '\n';  // true
    std::cout << dimensions_v<std::vector<std::array<std::vector<int>, 2>>>; // 3
}
Cruz Jean
fuente
¿Qué sucede si quiero que esto funcione para todos los contenedores anidados y no solo para los vectores? ¿Hay una manera fácil de hacer que eso suceda?
tjwrona1992
@ tjwrona1992 Sí, literalmente podrías copiar y pegar la std::vector<T>especialización y cambiarla a otro tipo de contenedor. Lo único que necesita es el caso base 0 para cualquier tipo para el que no se especialice
Cruz Jean
Quise
@ tjwrona1992 Oh, para eso necesitarías usar las funciones SFINAE constexpr. Crearé un prototipo para eso y lo agregaré como edición.
Cruz Jean
@ tjwrona1992, ¿cuál es su definición de contenedor?
Evg
15

Suponiendo que un contenedor es cualquier tipo que tenga value_typey iteratortipos de miembros (los contenedores de biblioteca estándar satisfacen este requisito) o una matriz de estilo C, podemos generalizar fácilmente la solución de Cruz Jean :

template<class T, typename = void>
struct rank : std::integral_constant<std::size_t, 0> {};

// C-style arrays
template<class T>
struct rank<T[], void> 
    : std::integral_constant<std::size_t, 1 + rank<T>::value> {};

template<class T, std::size_t n>
struct rank<T[n], void> 
    : std::integral_constant<std::size_t, 1 + rank<T>::value> {};

// Standard containers
template<class T>
struct rank<T, std::void_t<typename T::iterator, typename T::value_type>> 
    : std::integral_constant<std::size_t, 1 + rank<typename T::value_type>::value> {};

int main() {
    using T1 = std::list<std::set<std::array<std::vector<int>, 4>>>;
    using T2 = std::list<std::set<std::vector<int>[4]>>;

    std::cout << rank<T1>();  // Output : 4
    std::cout << rank<T2>();  // Output : 4
}

Los tipos de contenedores pueden restringirse aún más si es necesario.

Evg
fuente
Esto no funciona para las matrices de estilo C
Cruz Jean
1
@CruzJean, claro. Por eso pregunté qué es un contenedor. De todos modos, se puede solucionar fácilmente con una especialización adicional, consulte la respuesta actualizada.
Evg
2
@Evg gracias. Hoy aprendí sobre std :: void_t! ¡Brillante!
marco6
2

Puede definir la siguiente plantilla de clase vector_depth<>que coincida con cualquier tipo:

template<typename T>
struct vector_depth {
   static constexpr size_t value = 0;
};

Esta plantilla principal corresponde al caso base que finaliza la recursión. Luego, defina su especialización correspondiente para std::vector<T>:

template<typename T>
struct vector_depth<std::vector<T>> {
   static constexpr size_t value = 1 + vector_depth<T>::value;
};

Esta especialización coincide con std::vector<T>y corresponde al caso recursivo.

Finalmente, defina la plantilla de función GetDepth(), que recurre a la plantilla de clase anterior:

template<typename T>
constexpr auto GetDepth(T&&) {
   return vector_depth<std::remove_cv_t<std::remove_reference_t<T>>>::value;
}

Ejemplo:

auto main() -> int {
   int a{}; // zero depth
   std::vector<int> b;
   std::vector<std::vector<int>> c;
   std::vector<std::vector<std::vector<int>>> d;

   // constexpr - dimension determinted at compile time
   constexpr auto depth_a = GetDepth(a);
   constexpr auto depth_b = GetDepth(b);
   constexpr auto depth_c = GetDepth(c);
   constexpr auto depth_d = GetDepth(d);

   std::cout << depth_a << ' ' << depth_b << ' ' << depth_c << ' ' << depth_d;
}

La salida de este programa es:

0 1 2 3
眠 り ネ ロ ク
fuente
1
Esto funciona para std::vector, pero por ejemplo GetDepth(v), donde vse intno se compilará. Sería mejor tener GetDepth(const volatile T&)y regresar vector_depth<T>::value. volatilesolo deja que cubra más cosas, siendo el máximo cv calificado
Cruz Jean
@CruzJean Gracias por la sugerencia. He editado la respuesta.
眠 り ネ ロ ク