후행 반환 형식과 reference type 제거
요즘 개발하는 프로젝트에서 필요하다보니 찾아다 쓰는 것들이 있는데, 대부분이 최신 C++ 문법들을 포함하고 있어서 새로운 방식을 익히게 되는 중이다.
이제야 쓰기 시작하다보니 너무 간단한 내용들은 인터넷에 이미 많아서 굳이 내가 올릴 필요도 없고, 그냥 난 내가 쓰면서 실제로 사용하게 된 케이스들만 골라서 기록으로 남길까 싶다.
첫 번째로 다룰 코드는 SafeAccess() 라는 간단한 함수를 구현하는 과정에서 얻게 됐다. 간단하니 전체 코드를 긁어왔다.
// Code 1. SafeAcess()
template<typename BufType>
inline static auto SafeAccess( BufType& buf, int x, int y, int level = 0 ) -> typename std::remove_reference< decltype( buf[0] ) >::type
{
if( PixelValidTest( x, y, s_width[ level ], s_height[ level ] ) == false ) return 0;
return buf[ y * s_width[ level ] + x ];
}
특정 cpp 파일의 전역 함수로써, 이미지 밉맵 비슷한 처리를 해야해서 만든 두 줄짜리 함수다. 딱히 해당 파일에 국한될 필요는 없지만, 또 굳이 이 파일 외부로 나갈 이유도 없어서 아직은 static으로 고정시켜뒀다. 그 김에 생긴 것도 심플해서 inline이 아닐 이유도 없어서 inline 명시도 해둔 거고.
이것이 템플릿으로 만들어진 것에서 유추할 수 있듯, 버퍼의 타입 종류가 다른 경우가 이 함수를 사용한 구현 중에 있다. BufType&의 변수 buf는 처음엔 BufType& 타입이 아니라 BufType* 타입이었다. 그에 따라 반환형은 BufType이었고.
// Code 2. First SafeAccess
template<typename BufType>
inline static BufType SafeAccess( BufType* buf, int x, int y, int level = 0 );
그런데 이런 식으로 정의한 함수를 사용해서 구현하는 중에 buf가 포인터 선언으로 뒀다간 번거롭게도 일반적인 용례에 따른 사용이 불가능하여 캐스팅이나 직접적인 메모리 접근같이 우회하는 방식으로 코딩해야 하는 경우가 생겼다.
좀 더 구체적으로 말하자면, BufType*으로 지칭 될 타입이 단순히 C++ 내장형의 포인터인 것이 아니라 어떤 구조체 자체를 함수의 인자로 사용할 경우가 나타난 것이다. 해당 구조체는 포인터마냥 [] 연산자를 지원하도록 구현되었지만 그 자체가 포인터는 아니었기 때문에 함수가 BufType* 인자로 요구할 경우에 함수에 사용될 수 있는 형태로 변환되지 않는 문제가 있었다.
// Code 3. Pointer type and structured type overrides [] operator
float* data;
float resultF = SafeAccess( data, ... ); // 일반적인 경우의 사용 예
typename sometype;
struct A {
inline sometype& operator[] ( int index );
} a;
...
sometype resultS = SafeAccess( a, ... ); // operator[]를 사용하도록 구현된 데이터의 사용 예
// a가 포인터가 아므로 컴파일 에러
불가능하다고 해서 편의를 목적으로 만든, 이제까지 잘 사용하던 구조체를 사용하지 않을 순 없는 노릇이었으므로, 함수에 인자로 전달될 타입으로써 지칭될 템플릿 인자 자체가 직접적인 포인터를 요구하지 않는 것으로 해결하는 것이 바람직했다. 마침 decltype()의 존재와 후행 반환 형식의 지정의 존재를 안지 얼마 되지 않았던 터라서 바로 여기에 적용해볼 수 있었다.
함수의 반환식에서 buf가 포인터마냥 [] 연산이 가능한 타입인 것을 가정하고 있으므로, 반환형은 buf[0]과 동일한 타입이어야 한다. 이와 같은 사항은 컴파일 타임에 추론 가능하므로 decltype()을 사용함으로써 해결할 수 있을 것이라 생각했다. 이미 후행 반환 형식의 존재와 필요성을 알고 있던 상황에서 내가 이렇게 코딩하진 않았지만 맥락은 이렇게 흘러간다.
template<typename BufType>
inline static decltype( buf[0] ) SafeAccess( BufType& buf, int x, int y, int level = 0 );
위와 같이 decltype( buf[0] )을 반환형이 놓일 자리에 두는 것은 아직 buf가 선언되어 나타나지 않은 상황에서 불가능하므로, 대신 auto를 반환형 선언 위치에 적어두고 함수의 시그네쳐 뒤에 -> decltype( buf[0] )을 두는 후행 반환 형식이란 문법이 사용되면 원하던 결과로써 buf[0]과 동일한 타입이 반환될 수 있을 것이었다.
template<typename BufType>
inline static auto SafeAccess( BufType& buf, int x, int y, int level = 0 ) -> decltype( buf[0] );
그러나 안타깝게도 컴파일 에러를 마주하고 말았는데, 원인은 decltype( buf[0] ); 이 애석하게도 BufType이 아니라 BufType&로 해석된 것이 문제였다. 어떤 타입의 포인터 혹은 배열로 유추된 BufType에 [] 연산자로 접근한 특정 원소에 대해서는 데이터를 수정할 수 있는 권한 또한 부여되기 때문이다. [] 연산자를 정의했던 데이터를 수정하기 위해선 복사시킬 것이 아닌 대상에 접근할 수 있도록 하는 것이 옳으므로, BufType& 가 추론된 결과였다. 미처 생각하지 못한 실수였다.
어쩐지 너무 쉽게도 새로 배운 걸 써먹게 된다 싶었더니 이런 데서 막히는가 싶었다. 그러나 곧, 너무 간단하게도 이를 해결할 방법이 떠올랐다. 템플릿을 이용해 어떤 타입의 레퍼런스를 인자로 받아 타입에서 레퍼런스를 뗀 타입을 새로 재정의하여 돌려주면 될 일이었다.
template<typename Type&>
struct Unreferenced { typedef Type UnrefType; };
그리고 너무 당연하게도 그런 기능을 이미 표준이라며 구현해둔 것이 있었다. std::remove_reference<>인데, 구현은 lvalue reference에 대해 위와 일치한 구현을 가진다. 표준에선 rvalue의 경우에 대한 것도 존재하므로 좀 더 많은 경우에 대응할 수 있었다.
아무튼 그렇게 맨 위에 완성된 코드와 같은 형태가 되었다. 사이즈가 그리 큰 변경이 아니라 그런지 기존의 구현에 충돌되지 않으면서도 매끄럽게 확장할 수 있었던 점이 마음에 든다.