행렬 0x04: C++ Matrix 구조체 디자인
Matrix.h 행렬 구조체 만들기
C++로 Matrix를 구현하기 위해, class
로 만들건지, struct
로 만들건지 고민할 수 있다. C++에서는 class
와 struct
는 동일하다.(컴파일러도 똑같이 처리한다) 단 하나의 차이는 Struct는 접근 지정자가 기본적으로 public이다. 둘은 동일한 기능을 가지고 차이는 거의 없지만 접근 지정자가 public
인 점과, C Style의 구조체 관례때문에, C++에서도 관습적으로 struct
는 멤버데이터만을 가진 자료형으로 쓰인다. 이 말은 struct
는 C처럼 POD(Plain Old Data)
로써 memcpy로도 데이터를 복사할 수 있어야함을 의미한다.
1. POD로 구현하기
우선 결론부터 이야기하자면 POD가 쓰이는 이유는 성능을 위해 memcpy를 쓸 수 있도록하기 위함이다.
POD(Plain Old Data)
란, 인스턴스의 메모리 레이아웃이 모두 연속적인 바이트 열을 가짐을 의미한다. 즉, 멤버데이터 중에 힙메모리를 가르키고 힙메모리에 데이터를 확장하는(class같은) 데이터는 없어야한다는 것이다. 이러한 POD는 Old라는 단어를 가진 것 처럼, 개체지향, 모던 프로그래밍에서 안쓰이는 건가? 싶을 수 있다. 하지만 하드웨어 레벨에서 개체를 복사할 때 더 효율적인 측면이 있기 때문에, 런타임 다향성이나 모던스러운 문법, 기술이 필요하지 않거나, 혹은 더 빠른 성능의 코드가 필요할 땐, POD가 빛을 발한다. POD는 복잡한 고민없이 데이터 자체로만 취급하고 그대로 복사(std::memcpy)할 수 있기 때문이다.
POD로 구현하기 위해 다음 조건이 필요하다
- 표준 레이아웃 타입
표준 레이아웃 타입은 C와 동일한 메모리 레이아웃을 가질 수 있는 타입이다. 다음과 같은 특성을 가진다
- 가상함수를 가지지 않는다.
- 모든 멤버 데이터(static 변수 제외)가 표준 레이아웃 타입이다.
- 가상 부모 클래스를 가지지 않는다.
- 모든 부모 클래스가 표준 레이아웃 타입이다.
- 첫번째 비정적 멤버로 부모 클래스와 동일한 타입의 데이터를 갖지 않는다.(메모리 정렬, 패딩 이슈)
- 레퍼런스 멤버를 갖지 않는다.
- 단 하나의 부모클래스를 가져야한다.
- 부모클래스는 멤버 데이터가 없어야한다.
- 모든 멤버 데이터는 같은 접근제한을 가진다.
- memcpy가 가능하다.
- C와 호환된다.
- trival 타입
생성자, 소멸자, 복사 생성자, 이동 생성자가 컴파일러가 만드는 형태라면 trival 타입이다. 즉 간단한(trival) 복사가 가능하다.
- 생성자 소멸자가 정의되지 않았다.
- 가상함수가 없어야 한다.
- 가상 부모 클래스가 없어야 한다.
- 모두 trival한 멤버 데이터야한다.
- memcpy가 가능하다.
- C와 호환되지 않을 수 있다.
class/struct가 POD인지 확인할려면 <type_traits>
의 std::is_pod
를 사용하면 된다.
정리하자면
- 메모리 레이아웃이 간단해야한다.
- 복사 관련 의가 없어야한다.
로 간단히 설명할 수 있다.
2. 제네릭 프로그래밍
제네릭 프로그래밍을 남용하는 것은 좋지 않지만 행렬 구조체를 만드는데 제네릭 프로그래밍을 매우 적절한 경우가 될 수 있다. 타입에 의존되지 않고 재사용성과 유연성을 높일 수 있어 복소수나 특정한 자료형을 사용할 때 유용할 수 있다. 따라서 제네릭 프로그래밍을 하기 위해선 함수와 구조체를 일반화하여 설계해야한다. 교육적인 목적으로도 좋은 시도가 될 수 있다. 제네릭하게 제대로 구현했다면 복소수 타입의 제네릭 인수를 사용하는데 문제가 없을 것이다.
3. 일관된 함수 디자인
함수가 입력된 값을 처리하는 방식은 크게 두가지 방식이 있다.
- 누산 함수 (Accumulator Function)
누산 함수는 함수를 사용함으로써, 개체의 상태를 갱신해 나가거나, 입력값으로 들어온 개체의 상태를 갱신해 나가는 방식이다.
Matrix<float, 2, 2> m = {0, 0, 0, 0}; m.Add({1, 2, 3, 4}); // m 개체의 상태를 갱신함.
- 순수 함수 (Pure Function)
순수 함수는 입력된 레퍼런스나 개체를 갱신하지 않고 새로운 값을 반환하는 함수를 의미한다. 함수 호출에 있어 사이드 이펙트가 없고, 등일한 입력에 대해 동일한 출력을 반환한다.
Matrix<float, 2, 2> test1 = {0, 0, 0, 0}; Matrix<float, 2, 2> test2 = {1, 1, 1, 1}; // test1, test2에 상태를 갱신하지 않고 새로운 값을 반환 Matrix<float, 2, 2> result = addMatrix(test1, test2);
일관된 멤버 함수 디자인이 중요한 이유는 Add
함수는 누산 함수고, Sub
함수는 순수 함수라면 사용할 때 매우 혼란스러울 수 있다. 따라서 멤버 함수가 어떤 유형의 함수를 사용하기로 했다면 일관성있게 모든 함수를 동일한 유형으로 통일하는 것이 좋다.
이번 프로젝트에서는 시그니처를 가진 멤버 함수는 가능한 누산 함수
로 작성하고, +
, -
와 같은 연산자 오버로드에서는 새로운 값을 반환하는 순수 함수
의 유형으로 작성할 계획이다.
그리고 누산 함수
는 새로운 값을 반환하지 않기 때문에 성능에 더 도움이 된다.
Matrix 프로토타입
template <class K, u64 N, u64 M>
struct Matrix
{
K data[N * M];
K& operator()(u64, u64);
const K& operator()(u64, u64) const;
void Add(const Matrix&);
void Sub(const Matrix&);
void Scl(const K&);
// etc...
};
template <class K, u64 N, u64 M>
Matrix<K, N, M> operator+(const Matrix<K, N, M>&, const Matrix<K, N, M>&);
template <class K, u64 N, u64 M>
Matrix<K, N, M> operator-(const Matrix<K, N, M>&, const Matrix<K, N, M>&);
template <class K, u64 N, u64 M>
Matrix<K, N, M> operator*(const Matrix<K, N, M>&, const K&);
template <class K, u64 N, u64 M>
Matrix<K, N, M> operator*(const K&, const Matrix<K, N, M>&);
//etc...
Matrix 구조체는 POD타입으로 설계되었으며, 제네릭 프로그래밍 기법을 활용하였다. 또한 일관된 함수디자인을 통해 코드의 가독성과 유지보수를 높이고자 한다.
Leave a comment