Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I get the depth of a multidimensional std::vector at compile time?

I have a function that takes a multidimensional std::vector and requires the depth (or the number of dimensions) to be passed in as a template parameter. Instead of hardcoding this value I would like to write a constexpr function that will take the std::vector and return the depth as an unsigned integer value.

For example:

std::vector<std::vector<std::vector<int>>> v = {     { { 0, 1}, { 2, 3 } },     { { 4, 5}, { 6, 7 } }, };  // Returns 3 size_t depth = GetDepth(v); 

This needs to be done at compile time though because this depth will be passed to the template function as a template parameter:

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

Is there any way to do this?

like image 925
tjwrona1992 Avatar asked Dec 26 '19 16:12

tjwrona1992


2 Answers

A classic templating problem. Here's a simple solution like how the C++ standard library does. The basic idea is to have a recursive template that will count one by one each dimension, with a base case of 0 for any type that is not a 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) 

So then you could use it like so:

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

Edit:

Ok, I've finished the general implementation for any container type. Note that I defined a container type as anything that has a well-formed iterator type as per the expression begin(t) where std::begin is imported for ADL lookup and t is an lvalue of type T.

Here's my code along with comments to explain why stuff works and the test cases I used. Note, this requires C++17 to compile.

#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 } 
like image 80
Cruz Jean Avatar answered Sep 28 '22 13:09

Cruz Jean


Assuming that a container is any type that has value_type and iterator member types (standard library containers satisfy this requirement) or a C-style array, we can easily generalize Cruz Jean's solution:

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 } 

Container types can be further restricted if needed.

like image 26
Evg Avatar answered Sep 28 '22 12:09

Evg