컴파일 타임에 다차원 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, 나는 크기를 찾지 않고 깊이를 찾고 있습니다. 매우 다른 두 가지. std::vector서로 얼마나 많은 들이 중첩 되어 있는지 알고 싶습니다 . 로 예를 들어 std::vector<std::vector<int>> v;, GetDepth(v);그것은 2 차원 벡터이기 때문에 2를 반환합니다. 크기는 관련이 없습니다.
tjwrona1992

4
semi-related : nested vector가 항상 최선의 방법은 아닙니다. 사용 사례에 따라 단일 평면 벡터의 수동 2D 또는 3D 인덱싱이 더 효율적일 수 있습니다. (외부 레벨에서 포인터를 쫓는 대신 정수 수학).
Peter Cordes

1
@PeterCordes 더 나은 효율성은 한 가지 측면 일뿐입니다. 다른 하나는 플랫 유형이 배열의 연속적인 특성을 더 잘 표현한다는 것입니다. 잠재적으로 다른 길이로 중첩 된 구조는 기본적으로 인접한 n 차원 초 사각을 나타내는 유형 불일치입니다.
Konrad Rudolph

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)

편집하다:

좋아, 모든 컨테이너 유형에 대한 일반적인 구현을 마쳤습니다. I는 식에 따라 잘 형성 반복자 타입 가지고 아무것도 컨테이너 타입을 정의 유의 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 기본 사례입니다
Cruz Jean

복사 / 붙여 넣기없이 하하, 템플릿 템플릿처럼
tjwrona1992

@ tjwrona1992 오, SFINAE constexpr 함수를 사용해야합니다. 이를 위해 프로토 타입을 만들어 편집으로 추가하겠습니다.
Cruz Jean

@ tjwrona1992, 컨테이너에 대한 정의는 무엇입니까?
Evg

15

컨테이너가있는 모든 유형이라고 가정 value_typeiterator회원 유형 (표준 라이브러리 컨테이너가이 요구 사항을 만족) 또는 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 스타일 배열이 작동하지 않습니다
크루즈 장에게

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 제안 해 주셔서 감사합니다. 답변을 편집했습니다.
眠 り ネ ロ ク
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.