Wie kann ich die Tiefe eines mehrdimensionalen std :: vector zur Kompilierungszeit ermitteln?

45

Ich habe eine Funktion, die mehrdimensional ist std::vectorund erfordert, dass die Tiefe (oder die Anzahl der Dimensionen) als Vorlagenparameter übergeben wird. Anstatt diesen Wert fest zu codieren, möchte ich eine constexprFunktion schreiben , die die std::vectorund die Tiefe als unsigned integerWert zurückgibt .

Zum Beispiel:

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

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

Dies muss jedoch zur Kompilierungszeit erfolgen , da diese Tiefe als Vorlagenparameter an die Vorlagenfunktion übergeben wird:

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

Gibt es eine Möglichkeit, dies zu tun?

tjwrona1992
quelle
4
Die Größe von a std::vectorist eine Laufzeitsache, keine zur Kompilierungszeit. Wenn Sie einen Container zur Kompilierungszeit benötigen, schauen Sie nach std::array. Ebenfalls; Beachten Sie, dass constexprnur Mittel „ können bei der Kompilierung ausgewertet werden“ - es gibt kein Versprechen , dass es wird sein. Es kann zur Laufzeit ausgewertet werden.
Jesper Juhl
5
@JesperJuhl, ich suche keine Größe, ich suche Tiefe. Zwei sehr unterschiedliche Dinge. Ich möchte wissen, wie viele std::vectors ineinander verschachtelt sind. Zum Beispiel mit std::vector<std::vector<int>> v;, GetDepth(v);würde 2 zurückkehren , da es sich um eine 2 - dimensionale Vektor ist. Die Größe spielt keine Rolle.
tjwrona1992
4
Halbbezogen: Verschachtelt vectorist nicht immer der beste Weg, um Dinge zu tun. Die manuelle 2D- oder 3D-Indizierung eines einzelnen flachen Vektors kann je nach Anwendungsfall effizienter sein. (Nur ganzzahlige Mathematik statt Zeigerjagd von den äußeren Ebenen.)
Peter Cordes
1
@PeterCordes Bessere Effizienz ist nur eine Facette. Ein anderer ist, dass ein flacher Typ die zusammenhängende Natur des Arrays besser darstellt. Eine verschachtelte Struktur (mit möglicherweise unterschiedlichen individuellen Längen) ist im Grunde eine Typfehlanpassung zur Darstellung eines zusammenhängenden n-dimensionalen Hyperrechtecks.
Konrad Rudolph
4
In Bezug auf die Nomenklatur verwendet die Standardbibliothek rankfür diese Abfrage Array-Typen (in Übereinstimmung mit der mathematischen Nomenklatur für Tensoren). Vielleicht ist das hier ein besseres Wort als "Tiefe".
dmckee --- Ex-Moderator Kätzchen

Antworten:

48

Ein klassisches Vorlagenproblem. Hier ist eine einfache Lösung wie die C ++ - Standardbibliothek. Die Grundidee besteht darin, eine rekursive Vorlage zu haben, die jede Dimension einzeln zählt, mit einem Basisfall von 0 für jeden Typ, der kein Vektor ist.

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

Dann könnten Sie es so verwenden:

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

Bearbeiten:

Ok, ich habe die allgemeine Implementierung für jeden Containertyp abgeschlossen. Beachten Sie, dass ich einen Containertyp als alles definiert habe, das einen wohlgeformten Iteratortyp gemäß dem Ausdruck hat, begin(t)in den std::beginfür die ADL-Suche importiert wird, und teinen l-Wert vom Typ hat T.

Hier ist mein Code zusammen mit Kommentaren, um zu erklären, warum Sachen funktionieren und welche Testfälle ich verwendet habe. Beachten Sie, dass zum Kompilieren C ++ 17 erforderlich ist.

#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
quelle
Was ist, wenn dies für alle verschachtelten Container und nicht nur für Vektoren funktionieren soll? Gibt es eine einfache Möglichkeit, dies zu erreichen?
tjwrona1992
@ tjwrona1992 Ja, Sie können die std::vector<T>Spezialisierung buchstäblich einfach kopieren, einfügen und in einen anderen Containertyp ändern. Das einzige, was Sie brauchen, ist der 0-Basisfall für jeden Typ, auf den Sie sich nicht spezialisiert haben
Cruz Jean
Ich meinte ohne Kopieren / Einfügen haha, wie Vorlage der Vorlage
tjwrona1992
@ tjwrona1992 Oh, dafür müssten Sie SFINAE constexpr-Funktionen verwenden. Ich werde einen Prototyp dafür erstellen und ihn als Bearbeitung hinzufügen.
Cruz Jean
@ tjwrona1992, wie definieren Sie einen Container?
Evg
15

Unter der Annahme , dass ein Behälter jeden Typ ist, hat value_typeund iteratorElementtypen (Standard - Bibliothek Behälter erfüllen diese Anforderung) oder einen C-Stil - Array, können wir leicht verallgemeinern Cruz Jean ‚s - Lösung:

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
}

Containertypen können bei Bedarf weiter eingeschränkt werden.

Evg
quelle
Dies funktioniert nicht für Arrays im C-Stil
Cruz Jean
1
@CruzJean, sicher. Deshalb habe ich gefragt, was ein Container ist. Wie auch immer, es kann leicht mit einer zusätzlichen Spezialisierung behoben werden, siehe die aktualisierte Antwort.
Evg
2
@ Evg danke. Heute habe ich von std :: void_t erfahren! Brillant!
März 6
2

Sie können die folgende Klassenvorlage definieren, die einem vector_depth<>beliebigen Typ entspricht:

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

Diese primäre Vorlage entspricht dem Basisfall, der die Rekursion beendet. Definieren Sie dann die entsprechende Spezialisierung für std::vector<T>:

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

Diese Spezialisierung entspricht einem std::vector<T>und entspricht dem rekursiven Fall.

Definieren Sie abschließend die Funktionsvorlage GetDepth(), die auf die obige Klassenvorlage zurückgreift:

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

Beispiel:

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

Die Ausgabe dieses Programms ist:

0 1 2 3
眠 り ネ ロ ク
quelle
1
Dies funktioniert für std::vector, aber zB GetDepth(v)wo vwird intnicht kompiliert. Wäre besser zu haben GetDepth(const volatile T&)und einfach zurückzukehren vector_depth<T>::value. volatileLässt es einfach mehr Dinge abdecken und ist maximal für den Lebenslauf qualifiziert
Cruz Jean
@CruzJean Danke für den Vorschlag. Ich habe die Antwort bearbeitet.
り ネ ロ ク