21.5.1.1. public ModuleParams 상속 구조와 생성자 초기화 리스트(Initializer List)
우리가 C++ 클래스를 다중 상속받아 CustomApp 모듈을 만들 때, 컴파일러는 객체의 뼈대를 메모리(Heap)에 올리는 과정에서 아주 엄격한 규칙(Order of Construction)을 요구한다.
“자식 객체가 태어나기 전에, 반드시 두 부모(ModuleBase, ModuleParams) 객체가 먼저 온전한 형태로 태어나 있어야 한다.”
이 과정에서 콧대 높은 ModuleParams 부모 클래스를 어떻게 달래어 태어나게 만들 것인가가 바로 핵심이다.
1. C++ 컴파일러의 묵시적(Implicit) 호출의 실패
만약 여러분이 초기화 리스트 문법을 모르고 아래처럼 순진하게 생성자를 작성했다면 어떤 일이 벌어질까?
// 펌웨어 빌드가 박살 나는 잘못된 생성자 구현
CustomApp::CustomApp()
{
// 여기서 멤버 변수들을 0으로 초기화하겠다!
_error_count = 0;
}
여러분의 눈에는 그저 _error_count를 0으로 만드는 순수한 함수처럼 보이겠지만, C++ 컴파일러는 저 코드 블록({)을 열기 직전에 뒷구멍에서 몰래 다음의 코드를 강제로 삽입(Injection)해 버린다.
// 컴파일러의 뇌내 망상 코드
CustomApp::CustomApp()
: ModuleBase() // 부모 1의 디폴트 생성자 호출 (성공)
, ModuleParams() // 부모 2의 디폴트 생성자 호출 시도 (실패!)
{
_error_count = 0;
}
문제는 ModuleParams 클래스 내부에 아무 인자도 받지 않는 깡통 생성자인 ModuleParams()가 아예 존재하지 않도록 설계되어 있다는 점이다.
PX4 코어 메인테이너들은 개발자들이 파라미터 연결 고리(Linked List) 정보를 빼먹고 무턱대고 객체를 생성하는 것을 원천 차단하기 위해 디폴트 생성자를 아예 지워버렸다(Delete). 결국 링커(Linker)는 “나는 저런 이름의 생성자를 본 적이 없다!“라며 시뻘건 빌드 에러를 뿜어내고 빌드를 중단시킨다.
2. 초기화 리스트(Initializer List): 부모의 탄생을 통제하라
이 컴파일러의 폭주를 막고 부모의 생성자에 내가 원하는 정확한 파라미터(인자)를 주입하려면, 함수 블록({)이 열리기 전에 쌍점(:)을 찍고 명시적으로 호출 방식을 지시하는 초기화 리스트(Initializer List) 문법을 사용해야만 한다.
// PX4의 완벽한 생성자 뼈대
CustomApp::CustomApp() :
ModuleParams(nullptr), // 1. ModuleParams 부모 생성자에 nullptr 주입!
_error_count(0), // 2. 내 멤버 변수들도 여기서 같이 초기화
_loop_perf(perf_alloc(PC_ELAPSED, "custom_app_loop")) // 3. 퍼포먼스 카운터 할당
{
// 이제 여기(괄호 안)로 진입할 때는 두 부모가 완벽하게 태어난 상태다.
}
이 콜론(:) 뒤의 영역은 객체의 뱃속에 아직 진입하기 전, 메모리에 클래스의 뼈대가 막 조립되고 있는 극초기(Pre-construction) 단계이다.
우리가 명시적으로 ModuleParams(nullptr) 이라고 지시함으로써, 컴파일러는 더 이상 깡통 디폴트 생성자를 찾지 않고 우리가 넘겨준 메모리 주소(여기서는 nullptr)를 들고 파라미터 클래스의 셋업을 무사히 마치게 된다.
(참고: 초기화 리스트에 적힌 변수나 부모 클래스들은 1, 2, 3 번호표 순서가 아니라 헤더 파일에 선언된 순서대로 엄격하게 할당된다는 강력한 C++ 문법 규칙을 기억하라.)
그렇다면 여기서 던져준 저 투박무심한 nullptr(널 포인터)는 내부적으로 어떤 파란을 일으키며 글로벌 파라미터 캐시 네트워크를 형성하게 되는 걸까?
다음 단원인 21.5.1.1.1에서 ModuleParams(nullptr)이 파라미터 연결고리(Linked List)의 최상위 노드로서 작동하는 그 경이로운 링킹(Linking)의 기계를 해부해 보자.