Bagaimana saya bisa mendapatkan kedalaman std :: vektor multidimensi pada waktu kompilasi?

45

Saya memiliki fungsi yang mengambil multidimensi std::vectordan membutuhkan kedalaman (atau jumlah dimensi) untuk dilewatkan sebagai parameter templat. Alih-alih hardcoding nilai ini saya ingin menulis constexprfungsi yang akan mengambil std::vectordan mengembalikan kedalaman sebagai unsigned integernilai.

Sebagai contoh:

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

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

Ini perlu dilakukan pada waktu kompilasi karena kedalaman ini akan diteruskan ke fungsi templat sebagai parameter templat:

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

Apakah ada cara untuk melakukan ini?

tjwrona1992
sumber
4
Ukuran a std::vectoradalah run-time thing, bukan compile-time. Jika Anda menginginkan wadah ukuran waktu kompilasi, lihat std::array. Juga; ingat bahwa constexprhanya berarti " dapat dievaluasi pada waktu kompilasi" - tidak ada janji bahwa itu akan terjadi. Ini dapat dievaluasi pada saat run-time.
Jesper Juhl
5
@JesperJuhl, saya tidak mencari ukuran, saya mencari kedalaman. Dua hal yang sangat berbeda. Saya ingin tahu berapa banyak std::vectoryang bersarang di dalam satu sama lain. Misalnya dengan std::vector<std::vector<int>> v;, GetDepth(v);akan mengembalikan 2 karena ini adalah vektor 2 dimensi. Ukurannya tidak relevan.
tjwrona1992
4
Semi-related: nested vectortidak selalu merupakan cara terbaik untuk melakukan sesuatu. Pengindeksan 2d atau 3d manual dari satu vektor datar dapat lebih efisien, tergantung pada kasus penggunaan. (Hanya bilangan bulat matematika bukannya mengejar-pointer dari tingkat luar.)
Peter Cordes
1
@PeterCordes Efisiensi yang lebih baik hanya satu sisi. Yang lain adalah bahwa tipe flat lebih baik mewakili sifat berdekatan array. Struktur bersarang (dari panjang individu yang berpotensi berbeda) pada dasarnya adalah tipe ketidakcocokan untuk mewakili, hyperrectangle n-dimensi yang berdekatan.
Konrad Rudolph
4
Nomenklatur-bijaksana perpustakaan standar digunakan rankuntuk permintaan ini pada tipe array (sesuai dengan nomenklatur matematika untuk tensor). Mungkin itu kata yang lebih baik di sini daripada "kedalaman".
dmckee --- ex-moderator kitten

Jawaban:

48

Masalah templating klasik. Berikut adalah solusi sederhana seperti bagaimana perpustakaan standar C ++. Ide dasarnya adalah untuk memiliki template rekursif yang akan menghitung satu per satu setiap dimensi, dengan kasus dasar 0 untuk semua jenis yang bukan vektor.

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

Jadi Anda bisa menggunakannya seperti ini:

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

Edit:

Ok, saya sudah menyelesaikan implementasi umum untuk semua jenis kontainer. Perhatikan bahwa saya mendefinisikan tipe kontainer sebagai segala sesuatu yang memiliki tipe iterator yang terbentuk dengan baik sesuai dengan ekspresi di begin(t)mana std::begindiimpor untuk pencarian ADL dan tmerupakan nilai jenisT .

Berikut kode saya bersama dengan komentar untuk menjelaskan mengapa hal-hal berfungsi dan kasus uji yang saya gunakan. Catatan, ini membutuhkan C ++ 17 untuk dikompilasi.

#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
sumber
Bagaimana jika saya ingin ini berfungsi untuk semua wadah bersarang dan bukan hanya vektor? Apakah ada cara mudah untuk mewujudkannya?
tjwrona1992
@ tjwrona1992 Ya, Anda bisa benar-benar hanya menyalin dan menempelkan std::vector<T>spesialisasi dan mengubahnya ke beberapa jenis wadah lainnya. Satu-satunya yang Anda butuhkan adalah kasus dasar 0 untuk semua jenis yang tidak Anda khususkan
Cruz Jean
Maksud saya tanpa menyalin / menempel haha, Suka templating templat
tjwrona1992
@ tjwrona1992 Oh, untuk itu Anda harus menggunakan fungsi constexpr SFINAE. Saya akan membuat prototipe untuk itu dan menambahkannya sebagai edit.
Cruz Jean
@ tjwrona1992, apa definisi Anda tentang sebuah wadah?
Evg
15

Dengan asumsi bahwa suatu wadah adalah segala jenis yang memiliki value_typedan iteratorjenis anggota (wadah perpustakaan standar memenuhi persyaratan ini) atau larik gaya-C, kita dapat dengan mudah menggeneralisasi solusi 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
}

Jenis wadah selanjutnya dapat dibatasi jika diperlukan.

Evg
sumber
Ini tidak berfungsi untuk array gaya-C
Cruz Jean
1
@RuzJean, tentu saja. Itu sebabnya saya bertanya apa wadah itu. Lagi pula, itu dapat dengan mudah diperbaiki dengan spesialisasi tambahan, lihat jawaban yang diperbarui.
Evg
2
@ Evg, terima kasih. Hari ini saya belajar tentang std :: void_t! Cemerlang!
marco6
2

Anda dapat menentukan templat kelas berikut vector_depth<>yang cocok dengan jenis apa pun:

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

Template primer ini terkait dengan case dasar yang mengakhiri rekursi. Kemudian, tentukan spesialisasi yang terkait untuk std::vector<T>:

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

Spesialisasi ini cocok dengan std::vector<T>dan sesuai dengan kasus rekursif.

Terakhir, tentukan templat fungsi GetDepth(),, yang menggunakan templat kelas di atas:

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

Contoh:

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

Output dari program ini adalah:

0 1 2 3
眠 り ネ ロ ク
sumber
1
karya ini untuk std::vector, tapi misalnya GetDepth(v)di mana vini inttidak akan dikompilasi. Akan lebih baik untuk memiliki GetDepth(const volatile T&)dan kembali vector_depth<T>::value. volatilebiarkan saja itu mencakup lebih banyak barang, menjadi cv maksimum yang berkualitas
Cruz Jean
@CruzJean Terima kasih atas sarannya. Saya sudah mengedit jawabannya.
眠 り ネ ロ ク