Come posso ottenere la profondità di uno std :: vector multidimensionale al momento della compilazione?


45

Ho una funzione che accetta un multidimensionale std::vectore richiede che la profondità (o il numero di dimensioni) sia passata come parametro del modello. Invece di codificare questo valore, vorrei scrivere una constexprfunzione che prenderà std::vectore restituisca la profondità come unsigned integervalore.

Per esempio:

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

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

Questo deve essere fatto al momento della compilazione perché questa profondità verrà passata alla funzione template come parametro template:

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

C'è un modo per fare questo?


4
La dimensione di a std::vectorè una cosa di runtime, non di compilazione. Se si desidera un contenitore di dimensioni in fase di compilazione, consultare std::array. Anche; ricorda che constexprsignifica solo " può essere valutato in fase di compilazione" - non c'è promessa che lo sarà . Può essere valutato in fase di esecuzione.
Jesper Juhl,

5
@JesperJuhl, non sto cercando le dimensioni, sto cercando la profondità. Due cose molto diverse. Voglio sapere quanti std::vectors sono nidificati l'uno nell'altro. Ad esempio std::vector<std::vector<int>> v;, GetDepth(v);restituisce 2 poiché è un vettore bidimensionale. La dimensione è irrilevante.
tjwrona1992,

4
Semi-correlato: nidificato vectornon è sempre il modo migliore per fare le cose. L'indicizzazione manuale 2d o 3d di un singolo vettore piatto può essere più efficiente, a seconda del caso d'uso. (Solo la matematica intera anziché la ricerca del puntatore dai livelli esterni.)
Peter Cordes,

1
@PeterCordes Una migliore efficienza è solo un aspetto. Un altro è che un tipo flat rappresenta meglio la natura contigua dell'array. Una struttura nidificata (di singole lunghezze potenzialmente diverse) è fondamentalmente una mancata corrispondenza del tipo per rappresentare un iperrangolo rettangolare contiguo.
Konrad Rudolph,

4
Dal punto di vista della nomenclatura la libreria standard utilizza rankper questa query sui tipi di array (in accordo con la nomenclatura matematica per i tensori). Forse questa è una parola migliore qui di "profondità".
dmckee --- ex gattino moderatore,

Risposte:


48

Un classico problema di templating. Ecco una soluzione semplice come la libreria standard C ++. L'idea di base è quella di avere un modello ricorsivo che conterà uno per uno per ogni dimensione, con un caso base di 0 per qualsiasi tipo che non sia un vettore.

#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)

Quindi puoi usarlo così:

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

Modificare:

Ok, ho finito l'implementazione generale per qualsiasi tipo di contenitore. Si noti che ho definito un tipo di contenitore come qualsiasi cosa che abbia un tipo iteratore ben formato secondo l'espressione in begin(t)cui std::beginviene importato per la ricerca ADL ed tè un valore di tipo T.

Ecco il mio codice insieme ai commenti per spiegare perché le cose funzionano e i casi di test che ho usato. Nota, questo richiede la compilazione di C ++ 17.

#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
}

Cosa succede se voglio che funzioni per tutti i contenitori nidificati e non solo per i vettori? C'è un modo semplice per farlo accadere?
tjwrona1992,

@ tjwrona1992 Sì, potresti letteralmente copiare e incollare la std::vector<T>specializzazione e cambiarla in un altro tipo di contenitore. L'unica cosa di cui hai bisogno è il caso 0 base per qualsiasi tipo per cui non sei specializzato
Cruz Jean il

Intendevo senza copiare / incollare ahah, come
creare un

@ tjwrona1992 Oh, per questo dovresti usare le funzioni constexpr di SFINAE. Io prototipo una cosa per questo e la aggiungo come modifica.
Cruz Jean,

@ tjwrona1992, qual è la tua definizione di contenitore?
Evg

15

Supponendo che un contenitore sia di qualsiasi tipo value_typee iteratortipo di membro (i contenitori di libreria standard soddisfano questo requisito) o un array in stile C, possiamo facilmente generalizzare la soluzione di 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
}

I tipi di contenitore possono essere ulteriormente limitati, se necessario.


Questo non funziona per le matrici in stile C
Cruz Jean,

1
@CruzJean, certo. Ecco perché ho chiesto cos'è un contenitore. Ad ogni modo, può essere facilmente risolto con un'ulteriore specializzazione, vedere la risposta aggiornata.
Evg

2
@Evg grazie. Oggi ho imparato a conoscere std :: void_t! Brillante!
marco6,

2

È possibile definire il seguente modello di classe vector_depth<> che corrisponde a qualsiasi tipo:

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

Questo modello primario corrisponde al caso base che termina la ricorsione. Quindi, definisci la sua specializzazione corrispondente per std::vector<T>:

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

Questa specializzazione corrisponde a std::vector<T>e corrisponde al caso ricorsivo.

Infine, definisci il modello di funzione GetDepth(), che ricorre al modello di classe sopra:

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

Esempio:

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

L'output di questo programma è:

0 1 2 3

1
Questo funziona per std::vector, ad esempio, ma GetDepth(v)in cui vè intnon verrà compilato. Sarebbe meglio avere GetDepth(const volatile T&)e solo tornare vector_depth<T>::value. volatilepermette solo di coprire più cose, essendo la massima qualifica CV
Cruz Jean

@CruzJean Grazie per il suggerimento. Ho modificato la risposta.
眠 り ネ ロ ク
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.