[C++20 모듈과 IOCP로 완성하는 MMORPG 서버] 4. CoreMacro 개선

MIN·2025년 9월 7일
0

C++20 & IOCP

목록 보기
4/5

깃허브 주소 : CPP20_GameServer

오늘은 개발을 하다가 함수형 매크로를 헤더 파일로 관리하는 방식이 크게 불편한 것은 아니었지만, 이왕 정리하는 김에 inline과 constexpr를 활용해 모듈 인터페이스 파일로 옮기기로 했다.

이번에 바꿀 대상은 CRASH 기능과 Lock 관련 함수형 매크로들이다. 앞으로도 새로운 매크로 유틸을 같은 방식으로 모듈 친화적인 형태로 바꿀 수 있도록 패턴을 잘 잡아둘 예정이다.

CRASH

결과 캡처

CRASH 수정 전

이 함수형 매크로를 모듈에서 export하려면 전처리 매크로 대신 함수로 대체하는 편이 맞다.
먼저 수정 후를 보여주고 차례차례 설명하겠다.

첫 번째 이미지

CRASH 수정 후(1)

두 번째 이미지

CRASH 수정 후(2)

constexpr void analysis_assume(bool) noexcept {}

constexpr은 [C++20] Three-Way Comparison / Consteval 연구 여기에서 자세하게 설명하기 때문에 생략하겠다.

__analysis_assume의 이식성 대체로, 정적 분석 힌트를 흉내 내는 자리표시자이며 런타임 동작에는 영향이 없다.

[[noreturn]] inline void crash_impl() noexcept
{
    volatile int* p = nullptr;
    *p = 0xDEADBEEF;
    for (;;) {} // [[noreturn]] 보장
}

실질적으로 CRASH을 일으키는 코드로, noreturn을 명시해서 이 코드가 호출되면 절대 돌아가지 않겠다고 컴파일러에게 선언했다.
그래서 컴파일러는 이 신호를 받아들이면, 제어 흐름 추론에 도움을 받기 때문에 최적화가 된다.

volatile 한정사는 컴파일러가 멋대로 최적화하는 부분을 제어하기 때문에 p에 대한 포인터가 쓰기에 최적화로 인해 제거되는 현상을 방지한 것이다.

마지막으로 무한 반복문을 통해 혹시나 CRASH가 일어나지 않더라도 반환 경로가 없음을 보장해서 noreturn의 약속을 지키는 코드로 마무리한다.

[[noreturn]] inline void CRASH(const char*  = "CRASH") noexcept
{
    crash_impl();
}

inline void ASSERT_CRASH(bool expr, const char* expr_text = "ASSERT_CRASH") noexcept
{
    if (!expr) 
    {
        (void)expr_text;      // 원하면 로깅에 사용
        analysis_assume(false);
        CRASH("ASSERT_CRASH");
    }
    else 
    {
        analysis_assume(true);
    }
}

기존 사용성을 유지하기 위해 CRASH와 ASSERT_CRASH 네이밍을 그대로 가져갔다.
사용 방법은 이전과 동일하고, 필요하면 메시지를 전달해 로깅에 활용할 수 있도록 매개변수를 남겨두었다.

LOCK

결과 이미지

Lock 수정 전

Lock 객체의 배열을 만들어서 그 객체의 인덱스 번호에 해당되는 객체에 해당하는 atomic의 CAS 작업을 통해서 로컬 락을 거는 행동을 한다.
그리고 현재 객체가 락을 건 포인터 타입 이름을 알수 있도록 설계를 했다.

이제 이것도 모듈 인터페이스 파일에서 호환이 되는 형태로 바꿔볼 것이다.

첫 번째 이미지 두 번째 이미지

위쪽: Lock 수정 후(1) / 아래쪽: Lock 수정 후(2)

template <std::size_t N> 
struct LockBox 
{
    std::array<Lock, N> locks; // 이동/복사 불필요
    Lock& operator[](std::size_t i) noexcept                { return locks[i]; }
    const Lock& operator[](std::size_t i) const noexcept  { return locks[i]; }
};

새로운 버전으로 만들 때, USE_LOCK 부분을 과감하게 삭제시켜서 사용하기 더 편하게 만들었다.
LockBox struct을 만들어서 정적 크기의 Lock 컨테이너를 만들었다.
이전에 사용했던 것보다 더 표준적이고 안전한 array 버전을 사용했다.

operator 버전도 만들어서 Lock& 객체에 바로 접근할 수 있게 만들었다.

template <std::size_t N>
[[nodiscard("READ_LOCK_IDX 반환")]] inline ReadLockGuard READ_LOCK_IDX(LockBox<N>& box, std::size_t idx,
        std::source_location loc = std::source_location::current())
{
    return ReadLockGuard(box[idx], loc.function_name());
}

이번에는 nodiscard 키워드가 나왔는데, 이건 반드시 이 함수에 변수를 바인딩하도록 부여하는 키워드이다.
그리고 C++20에서 새롭게 나온 source_location을 사용했다.
이건 호출 지점의 정보를 파악하기 위해서 나온 API임으로 function_name()만 넘겨서 각 Lock의 이름을 인자로 사용해서 이번에 만든 DeadLockProfiler에 정확한 호출 함수명을 남긴다.

template <std::size_t N>
[[nodiscard("WRITE_LOCK_IDX 반환")]] inline WriteLockGuard WRITE_LOCK_IDX(LockBox<N>& box, std::size_t idx,
          std::source_location loc = std::source_location::current())
  {
      return WriteLockGuard(box[idx], loc.function_name());
  }

WRITE_LOCK_IDX는 아까 소개한 READ_LOCK_IDX와 차이가 없기 때문에 생략하겠다.

template <std::size_t N>
[[nodiscard("READ_LOCK 반환")]] inline ReadLockGuard READ_LOCK(LockBox<N>& box,
        std::source_location loc = std::source_location::current())
	{
        return READ_LOCK_IDX(box, 0, loc);
    }
    
template <std::size_t N>
[[nodiscard("WRITE_LOCK 반환")]] inline WriteLockGuard WRITE_LOCK(LockBox<N>& box,
        std::source_location loc = std::source_location::current())
	{
        return WRITE_LOCK_IDX(box,0, loc);
    }

READ_LOCK과 WRITE_LOCK은 0번 Lock에 대한 편의 래퍼 함수로 만들었다.
사용할 때는 편하게 LockBox을 맴버 변수로 선언하고 함수를 사용하기 전에 미리 READ_LOCK과 WRITE_LOCK을 선언하면 된다.

결과물

전에 사용한 TestLock을 가져와서 이번에 바꾼 버전으로 교체하는 작업을 했다.

캡처 이미지

결과 코드

이제 USE_LOCK을 선언할 필요 없이 사용할 코드에 저렇게 변수를 선언하면 된다.
작동 방식은 RAII처럼 사용하기 때문에 변수를 선언하고 블록을 벗어나면 저절로 락이 해제되는 형태이다.

마지막으로 결과물을 띄우고 끝을 내겠다.

결과 이미지

결과물

참고

Inflearn [Rookiss][C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버

profile
게임개발자(진)

0개의 댓글