Як я можу отримати глибину багатовимірного std :: вектора під час компіляції?


45

У мене є функція, яка займає багатовимірність std::vectorі вимагає передавати глибину (або кількість розмірів) як параметр шаблону. Замість жорсткого кодування цього значення я хотів би написати constexprфункцію, яка буде приймати std::vectorі повертати глибину як unsigned integerзначення.

Наприклад:

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

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

Це потрібно зробити під час компіляції, хоча ця глибина буде передана функції шаблону як параметр шаблону:

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

Чи можна це зробити?


4
Розмір а std::vector- це річ часу, а не компіляція. Якщо ви хочете контейнер розміру за часом компіляції, зверніть увагу std::array. Також; пам’ятайте, що constexprлише засоби " можуть бути оцінені під час компіляції" - немає обіцянки, що це буде . Це може бути оцінено під час виконання.
Jesper Juhl

5
@JesperJuhl, я не шукаю розміру, я шукаю глибину. Дві дуже різні речі. Я хочу знати, скільки std::vectors вкладено один в одного. Наприклад std::vector<std::vector<int>> v;, GetDepth(v);повертає 2, оскільки це двомірний вектор. Розмір не має значення.
tjwrona1992

4
Напівзалежно: вкладені vectorне завжди найкращий спосіб робити речі. Ручне 2d або 3d-індексування одного плоского вектора може бути ефективнішим, залежно від випадку використання. (Просто ціла математика замість переслідування вказівників із зовнішніх рівнів.)
Пітер Кордес

1
@PeterCordes Краща ефективність - це лише одна грань. Інший - плоский тип краще представляє суміжний характер масиву. Вкладена структура (потенційно різної індивідуальної довжини) є принциповим невідповідністю для представлення суміжного, n-мірного гіперпрямокутника.
Конрад Рудольф

4
Стандартна бібліотека використовує rankдля цього запиту типи масивів (відповідно до математичної номенклатури для тензорів). Можливо, тут краще слово, ніж «глибина».
dmckee --- кошеня колишнього модератора

Відповіді:


48

Класична шаблонна проблема. Ось просте рішення, як, наприклад, стандартна бібліотека C ++. Основна ідея полягає в тому, щоб мати рекурсивний шаблон, який буде рахувати один за одним кожен вимір, із базовим випадком 0 для будь-якого типу, який не є вектором.

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

Тоді ви можете використовувати його так:

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

Редагувати:

Гаразд, я закінчив загальну реалізацію для будь-якого типу контейнерів. Зауважте, що я визначив тип контейнера як усе, що має добре сформований тип ітератора відповідно до виразу, begin(t)куди std::beginімпортується для пошуку ADL і tє значенням типу T.

Ось мій код разом із коментарями, щоб пояснити, чому робота працює та тестові приклади, які я використовував. Зверніть увагу, для компіляції потрібен 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
}

Що робити, якщо я хочу, щоб це працювало для всіх вкладених контейнерів, а не лише для векторів? Чи існує простий спосіб зробити це?
tjwrona1992

@ tjwrona1992 Я, ви можете буквально просто скопіювати та вставити std::vector<T>спеціалізацію та змінити її на інший тип контейнера. Єдине, що вам потрібно - це 0 базових
Cruz Jean

Я мав на увазі без копіювання / вставки ха-ха, як шаблони шаблону
tjwrona1992

@ tjwrona1992 О, для цього вам потрібно буде використовувати функції SFINAE constexpr. Я буду прототипувати щось для цього і додати його як редагування.
Крус Жан

@ tjwrona1992, яке ваше визначення контейнера?
Evg

15

Якщо припустити, що контейнер - це будь-який тип, який має типи value_typeта iteratorелементи (стандартні контейнери бібліотеки задовольняють цій вимозі) або масив у стилі C, ми можемо легко узагальнити рішення 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
}

Типи контейнерів можна додатково обмежити за потреби.


Це не працює для масивів у стилі С
Cruz Jean

1
@CruzJean, звичайно. Ось чому я запитав, що таке контейнер. У будь-якому випадку це можна легко виправити за допомогою додаткової спеціалізації, дивіться оновлену відповідь.
Evg

2
@Evg дякую Сьогодні я дізнався про std :: void_t! Блискуче!
marco6

2

Ви можете визначити такий шаблон класу, vector_depth<>який відповідає будь-якому типу:

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

Цей основний шаблон відповідає базовому випадку, який закінчує рекурсію. Потім визначте відповідну спеціалізацію для std::vector<T>:

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

Ця спеціалізація відповідає рівню std::vector<T> і відповідає рекурсивному випадку.

Нарешті, визначте шаблон функції,, GetDepth()який вдається до шаблону класу вище:

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

Приклад:

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

Результатом цієї програми є:

0 1 2 3

1
це працює для std::vector, але, наприклад, GetDepth(v)де vце intне буде компілюватися. Було б краще мати GetDepth(const volatile T&)і просто повернутися vector_depth<T>::value. volatileпросто дозволяє висвітлювати більше речей, маючи максимальну кваліфікацію
Cruz Jean

@CruzJean Дякую за пропозицію. Я відредагував відповідь.
眠 り ネ ロ ク
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.