コンパイル時に多次元std :: vectorの深さを取得するにはどうすればよいですか?


45

多次元を取りstd::vector、深さ(または次元数)をテンプレートパラメーターとして渡す必要がある関数があります。この値をハードコーディングする代わりに、を使用して深さを値として返すconstexpr関数を作成します。std::vectorunsigned 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
aのサイズはstd::vector実行時のものであり、コンパイル時のものではありません。コンパイル時のサイズのコンテナーが必要な場合は、を参照してくださいstd::array。また; constexpr「コンパイル時に評価される可能性がある」という意味だけであることを忘れないでください。そうなるとは約束されていません。実行時に評価される場合があります。
Jesper Juhl

5
@JesperJuhl、私はサイズを探しているのではなく、深さを探しています。2つの非常に異なるもの。std::vector相互にネストされているの数を知りたい。例えばstd::vector<std::vector<int>> v;GetDepth(v);それは2次元のベクトルであるので、2を返します。サイズは関係ありません。
tjwrona1992

4
準関連:入れ子vectorが常に最善の方法であるとは限りません。単一のフラットベクトルの手動2dまたは3dインデックスは、ユースケースに応じてより効率的になります。(外部レベルからのポインタ追跡の代わりに整数演算のみ。)
Peter Cordes

1
@PeterCordes効率の向上は1つの側面にすぎません。もう1つは、フラットタイプが配列の隣接する性質をよりよく表すことです。ネストされた構造(潜在的に異なる個々の長さの)は、基本的に、隣接するn次元の超長方形を表すための型の不一致です。
Konrad Rudolph、

4
命名法に関しては、標準ライブラリはrank配列タイプに対するこのクエリに使用します(テンソルの数学的な命名法と一致しています)。おそらく、ここでは「深さ」よりも良い言葉です。
dmckee ---元モデレーターの子猫

回答:


48

古典的なテンプレートの問題。以下は、C ++標準ライブラリのように簡単な解決策です。基本的な考え方は、各次元を1つずつカウントする再帰的なテンプレートを作成することです。ベクトルではないすべての型の基本ケースは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)

編集:

さて、私はあらゆるコンテナタイプの一般的な実装を終えました。私が表現に従って整形イテレータ型を持つものとして、コンテナタイプを定義したことを注意ADLのルックアップのためにインポートされたタイプの左辺値です。begin(t)std::begintT

これが、コードが機能する理由と使用したテストケースを説明するコメント付きのコードです。これをコンパイルするには、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の基本ケースです
クルーズジャン

テンプレートのテンプレート化のように、私はコピー/貼り付けなしで意味しました
tjwrona1992 '26

@ tjwrona1992ああ、そのためにはSFINAE constexpr関数を使用する必要があります。そのためのプロトタイプを作成し、編集として追加します。
クルーズジーン

@ tjwrona1992、コンテナの定義は何ですか?
Evg

15

コンテナに含まれる任意のタイプであると仮定するvalue_typeと、iteratorメンバーの種類(標準ライブラリコンテナは、この要件を満たす)またはCスタイルの配列、我々は容易に一般化することができるクルーズジーンの溶液は:

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
}

必要に応じて、コンテナタイプをさらに制限できます。


これは、Cスタイルの配列では機能しません
Cruz Jean

1
@CruzJean、確かに。だからコンテナとは何かと尋ねました。とにかく、それは追加の専門分野で簡単に修正できます。更新された回答を参照してください。
19

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>::valuevolatileより多くのものをカバーできるようにし、最大のcv資格を取得
Cruz Jean

@CruzJean提案をありがとう。回答を編集しました。
眠りネロク
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.