통합개발환경: DevCpp
언어: C++20
운영체제: Windows11 Home
컴파일러: g++
우리는 흔히 배열의 크기를 구할때 다음과 같은 방법을 사용할 수 있습니다.
int main() {
int arr[5];
int size = sizeof(arr) / sizeof(arr[0]); // 5
}
당연하게도 이 값을 입출력으로 찍어보면 "5"가 나올겁니다.
그렇다면 위 배열의 차원의 찻수인 "1"을 구하려면 어떻게 해야할까요??
다음과 같은 배열이 있다고칩시다.
int arr[3][2][1]; // 배열의 차원은 3차원 즉, 3
이럴때 배열의 차원은 3차원 즉, 차원의 찻수는 3입니다.
이러한 차원 수를 구할 방법에 관하여 포스팅해보겠습니다.
c++이다보니 std::vector의 다차원배열에 관한 내용도 포함됩니다.
사실 저가 머신러닝에 관하여 공부하기 전까지만해도, 이러한 내용은 프로그래밍에서 생각하지도 않았습니다.
최근 파이썬으로 다차원 리스트를 사용하다보면 가끔 다차원배열을 프로그래머가 직접 명시해주지않아도 라이브러리에서 내부적으로 배열의 차원의 수를 파악하고 그만큼 반복문의 수를 준다던지, 파라미터로 차원의 방향을 정해주기만해도 알아서 라이브러리에서 역할을 수행해주죠..
저는 그래서 c++에서도 배열의 차원 수를 얻어낼 방법이 존재할까??
라는 의문에서 만들기 시작했습니다.
우선 일반적인 배열은 c++ 공식라이브러리인 <type_traits>에 정의된 std::rank함수를 사용할겁니다.
std::rank는 말그대로 그 배열의 차원의 수를 반환합니다. 다음과 같이 사용이 가능합니다.
#include <iostream>
#include <type_traits>
template<class dataType>
constexpr inline std::size_t getDimension_Array(dataType const& Array){
return std::rank<dataType>::value;
}
int main() {
int arr[5][4][3][2][1];
std::cout << getDimension_Array(arr) << '\n';
}
출력해보면 "5"가 나옵니다.
그냥 단순하게 랩핑한 함수이기때문에 그다지 설명할것은없습니다.
그러면...
만약 다차원 벡터배열이 존재한다고하면은 어떻게 알아낼 수 있을까요?
std::vector<std::vector<std::vector<int>>> vec; // 예시
우선 벡터는 클래스이기때문에 위와같이 std::rank를 사용하는것으로는 제대로된 결과값을 얻을수 없을 뿐만 아니라,
내부적으로 실제로 저장된 데이터의 주소에 접근한다 하더라도,
그 데이터는 포인터형식으로 이루어져있기때문에 이 방법또한 불가능하죠..
그래서 저는 좀 간단한 방법을 생각해봤습니다.
#include <iostream>
#include <vector>
#include <type_traits>
template<class dataType>
constexpr inline std::size_t getDimension_Vector(dataType const& Vector, std::size_t size){
auto CopiedVector{Vector};
if constexpr(std::is_class<dataType>::value){
if(CopiedVector.empty()) CopiedVector.push_back({});
return getDimension_Vector(CopiedVector[0], ++size);
}
return --size;
}
template<class dataType>
constexpr inline std::size_t getDimension_Vector(dataType const& Vector){
if constexpr(std::is_class<dataType>::value){
auto CopiedVector{Vector};
if(CopiedVector.empty()) CopiedVector.push_back({});
return getDimension_Vector(CopiedVector, 1);
}
return 1;
}
class cClass{};
int main() {
std::vector<std::vector<std::vector<std::vector<int>>>> vec;
std::vector<std::vector<std::vector<cClass>>> new_vec;
std::cout << getDimension_Vector(vec) << '\n'; // Ok
// std::cout << getDimension_Vector(new_vec) << '\n'; // Error
}
메커니즘은 매우 간단합니다.
우선 첫번째로 호출되는 함수는 아랫쪽입니다.
"복사된 객체로부터 이것이 클래스인가?"라고 물어봅니다.
만약 맞다면 그 복사된 객체에다가 빈 데이터를 집어넣고 복사된 객체를 파라미터로 전달해줍니다.
아니라면, 1을 반환합니다.
여기서 객체를 복사한 이유는 원본배열에 데이터에 간섭을 주지않기위함이고,
빈 데이터를 넣은 이유는 오버플로우를 방지하기 위함입니다.
다음부터는 쭈우욱~ 위쪽 함수가 재귀적으로 호출되게 됩니다.
요기서도 본래객체에 영향을 주지않기위해, 객체를 복사합니다.
복사한후, 이후에 수행되는 명령은 아랫쪽함수와 같습니다.
다른점이 있다면, 값을 반환할때, 자신의 0번째 인덱스에 포함되는 데이터를 파라미터로 전달한다는 점입니다.
그 이유는 자신의 차원보다 한단계 낮은 차원을 전달해나가며 0차원, 즉 배열이 모두 벗겨질때까지 반복하는것이죠.
하지만...
이러한 방법에도 한계점은 분명히 존재합니다.
바로 벡터 배열이 가지고있는 배열의 타입이 int, float같은 것이아닌, 클래스라면 오작동을 일으키게되는거죠.
그래서 저는 더욱 안정된 방법을 생각했습니다.
"클래스 배열이더라도 알아낼 수 있을까??"
두번째 방법은 다음과 같은 방법을 생각했습니다.
#include <iostream>
#include <vector>
#include <type_traits>
template<class dataType, class originType>
constexpr inline auto __getDimension_Vector_s(dataType const& Vector, std::size_t size) -> std::size_t{
auto CopiedVector{Vector};
if constexpr(!std::is_same_v<decltype(CopiedVector), originType>){
if(CopiedVector.empty()) CopiedVector.push_back({});
return __getDimension_Vector_s<decltype(CopiedVector[0]), originType>(CopiedVector[0], ++size);
}
return --size;
}
template<class dataType, class originType>
constexpr inline auto __getDimension_Vector_s(dataType const& Vector) -> std::size_t{
if constexpr(!std::is_same_v<dataType, originType>){
auto CopiedVector{Vector};
if(CopiedVector.empty()) CopiedVector.push_back({});
return __getDimension_Vector_s<dataType, originType>(CopiedVector, 1);
}
return 1;
}
template<class originType>
constexpr inline auto getDimension_Vector_s(auto const& Vector) -> std::size_t{
return __getDimension_Vector_s<decltype(Vector), originType>(Vector);
}
class cClass{};
int main() {
std::vector<std::vector<std::vector<std::vector<int>>>> vec;
std::vector<std::vector<std::vector<cClass>>> new_vec;
std::cout << getDimension_Vector_s<int>(vec) << '\n'; // Ok
std::cout << getDimension_Vector_s<cClass>(new_vec) << '\n'; // Ok
}
이런식으로하면 벡터가 가지고있는 데이터타입이 클래스라 할지라도, 모두 정상적으로 작동합니다.
전과의 차이가 있다면, "클래스인가?"를 물어보는것이아닌, "컴파일타임으로 알려준 타입이 현재 벡터 배열의 타입과 다른가?"를 물어보는것으로 조건을 바꿨습니다.
이번에는 함수에 컴파일타임으로 해당 벡터의 데이터타입이 가리키는것이 int인지 클래스인지 float인지 알려주고,
그 타입들이 매번 함수를 재귀적으로 호출할때마다, 한 차원씩 깎여져나가는 객체와 타입이 같아질때까지 반복합니다.
그러면 최종적으로 모든 과정을 반복했을때 최종적으로 해당 벡터 배열의 차원의 수를 얻을 수 있게됩니다.
두번째 방법을 그대로 사용하고 다음과 같이 우리가 두번째로 구현한 함수에 std::vector
가 아닌 이상한 클래스를 집어넣으면, 이상한곳에서 에러가 납니다.
cClass c;
std::cout << getDimension_Vector_s<cClass>(c) << '\n';
이 문제는 함수가 템플릿으로 넘겨받은 타입이 std::vector
인지 몰랐기때문에 컴파일타임에 에러가 난것입니다.
이 문제를 해결하는 방법은 매우 간단합니다. 사전에 std::vector
만 집어넣는것이 가능하게 해주면 됩니다.
template<typename T, typename = void>
struct is_iterator
{
static constexpr bool value = false;
};
template<typename T>
struct is_iterator<T, typename std::enable_if<!std::is_same<typename std::iterator_traits<T>::value_type, void>::value>::type>
{
static constexpr bool value = true;
};
template<class T>
concept OnlyBeVector = is_iterator<decltype(std::declval<T>().begin())>::value &&
is_iterator<decltype(std::declval<T>().end())>::value &&
std::is_integral<decltype(std::declval<T>().size())>::value;
이런식으로 반복자를 concept
에서 감지할 수 있도록 미리 is_iterator<T>::value
를 정의해줍니다.
그러면 우리는 이제 함수에 들어올 타입이 std::vector
의 조건에 맞게 타이트하게 짜면 됩니다.
그리고 두번째방법의 함수를 이런식으로 수정해줍니다.
template<class originType>
constexpr inline auto getDimension_Vector_s(OnlyBeVector auto const& Vector) -> std::size_t{
return __getDimension_Vector_s<decltype(Vector), originType>(Vector);
}
이러면 이제 정확히 std::vector
가 들어오지 않을때 바로 프로그래머가 직관적으로 알수 있게됩니다.
재미있었다.
나중에는 좀더 응용해서 n차원 벡터 배열이 들어왔을때 모든 값을 순회하는 방법에 관하여 연구할것이다.
궁금한 부분있으면 댓글로 질문주세요..!
그럼 안녕~~!!~!~!