CMake FetchContent의 target이 중복되는 문제

CMake의 FetchContent는 빌드 과정까지 처리해 주기에 git-submodule 보다 편리한 방식인 것 같다. 다만, version 3.22.1 기준으로 여러 프로젝트 들이 동일한 target이름을 중복해서 선언하는 경우에는 이를 잘 처리 하지 못하고 위와 같은 에러를 뱉고 종료되는 문제가 있다.

-- Found systemd via pkg-config.
CMake Error at build/_deps/grpc-src/CMakeLists.txt:667 (add_custom_target):
  add_custom_target cannot create target "tools" because another target with
  the same name already exists.  The existing target is a custom target
  created in source directory
  "<absolute_path_to_the_build_dir>/_deps/vsomeip-src".
  See documentation for policy CMP0002 for more details.

위의 오류는 COVESA의 vsomeip project와 gRPC를 FetchContent로 구성하는 중에 발생한 것인데, 두 프로젝트 모두 “tools”라는 custom target을 선언하기 때문에 중복으로 오류가 발생한 것이다.

아쉽게도 현재로써는 깔끔하게 해결하는 방법은 없는 것 같고, 두 프로젝트 중 하나의 custom target 이름을 변경해서 중복되지 않도록 해 주는 패치를 작성해서 FetchContent의 PATCH_COMMAND에 인자로 넣어주는 방법으로 회피가 가능하다.

먼저 vsomeip project의 custom target인 tools를 vsomeip_tools로 변경하는 패치를 다음과 같이 작성하고 fix_custom_target.patch로 저장한다.

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3501e02..0467426 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -654,7 +654,7 @@ add_subdirectory( examples/routingmanagerd )
 endif()
 
 # build tools
-add_custom_target( tools )
+add_custom_target( vsomeip_tools )
 add_subdirectory( tools )
 
 # build examples
-- 
2.34.1

다음으로 FetchConetnet에 PATCH_COMMAND argument를 명시한다.

# VSOME/IP
set(
    fix_custom_target
    git apply ${CMAKE_CURRENT_SOURCE_DIR}/fix_custom_target.patch
)
FetchContent_Declare(
    vsomeip
    GIT_REPOSITORY https://github.com/COVESA/vsomeip.git
    GIT_TAG <GIT_TAG>
    PATCH_COMMAND ${fix_custom_target}
)

FetchContent_MakeAvailable(vsomeip gRPC)

이제 cmake를 실행하면 patch가 적용되어 중복되는 target 이름 문제를 해결할 수 있다.

하지만 이 해결책은 아직 깔끔하진 않은데 그 이유는 처음 cmake를 실행하면 patch가 적용되면서 잘 되지만 두번째로 cmake를 실행 할 때는 이미 적용된 patch 때문에 git apply 명령어가 실패 하기 때문에 매번 patch가 적용된 directory(_deps/vsomeip-src)로 이동해서 git checkout -f를 실행해 주어야 하기 때문이다.

최종버전

git apply의 –reverse –check 옵션을 사용하면 패치가 적용되어 있는지 여부를 검사할 수 있으니 이 것을 이용해서 다음과 같은 간단한 shell script를 만들고 apply_patch_if_not.sh로 저장한다.

#!/usr/bin/env sh
# apply_patch_if_not.sh
# Apply given patch if not already applied.
#
#                         - Sep 2024, litcoder

if [ -z $1 ]; then
  echo "Usage: $0 <patch file path>"
  exit 1
fi

# Check if the patch was already applied.
git apply --reverse --check $1
if [ $? -eq 0 ]; then
  echo "The patch already applied, skip."
  exit 0
else
  # Apply the patch
  git apply $1
fi

이제 앞에서 작성한 FetchContent_Declare가 이 script를 부르도록 변경한다.

# VSOME/IP
set(
    fix_custom_target
    apply_patch_if_not.sh ${CMAKE_CURRENT_SOURCE_DIR}/fix_custom_target.patch
)
FetchContent_Declare(
    vsomeip
    GIT_REPOSITORY https://github.com/COVESA/vsomeip.git
    GIT_TAG <GIT_TAG>
    PATCH_COMMAND ${fix_custom_target}
)

FetchContent_MakeAvailable(vsomeip gRPC)

C++에서 오버라이드된 함수가 안불러져요

Data라고 하는 정보를 저장하는 클래스들의 기초 클래스가 있다고 해보자. Data의 type에 따라 로딩 하는 방법이 서로 달라지기 때문에 loadData()라고 하는 가상함수를 하나 정의해 두고, 상속받는 클래스들은 자기가 loading하는 방법을 정의하도록 구성한다.

class Data {
public:
  Data(int v) { loadData(v); }
  void dispValue() { cout << "Value is " << _val << endl; }

protected:
  // 상속받는 클래스에서 구현해야 함.
  // 호출되면 안됨.
  virtual bool loadData(int v) {
    cout << "Default loader, do not call me!" << endl;
    return false;
  }
  int _val = 0;
};

Data에서 상속받는 DerivedData 클래스는 자기 버전의 loadData()를 오버라이드해서 구현한다.

class DerivedData : public Data {
public:
  DerivedData(int v) : Data(v) {}

protected:
  bool loadData(int v) override {
    cout << "DerivedData1::loadData()" << endl;
    _val = v;
    return true;
  }
};

DerivedData를 생성하고 100으로 초기화해서 결과를 보여주도록 main()을 작성한다.

int main(void) {
  auto d = DerivedData(100);
  d.dispValue();
}

이제 컴파일하고 실행해보자.

$ g++ ./main.cpp 
$ ./a.out 
Default loader, do not call me!
Value is 0

예상과 달리 DerivedData의 loadData()가 아닌 Data의 loadData()가 불리고 있다. 분명히 DevriedData의 인스턴스를 만들었는데도 오버라이드한 멤버함수가 동작하지 않는 이유는 무엇일까?

이 예제에서는 Data에서 상속 받은 DerivedData 클래스의 생성자가 호출되기 전에 Data의 생성자가 먼저 호출된다. Data클래스의 생성자에서는 가상함수로 정의된 loadData()의 오버라이드 구조를 따라 내려가지 않고 현재 클래스인 Data에 정의되어 있는 loadData() 를 바인딩한다. 아직 DerrivedData 클래스가 초기화 되지 않은 시점이기 때문에 기초 클래스의 생성자에서 파생 클래스의 오버라이드 멤버를 바인딩하는 것은 아직 알지 못하는 정보를 참조해야 하는 일이되기 때문이다.

Scott Meyers의 명서인 Effective C++에서도 이 내용을 언급하고 있었다. “Item 9: Never call virtual functions during construction or destruction”(생성자나 소멸자에서 가상함수를 절대 호출하지 말것) 아마도 다시 정독할 때가 됐나 보다.

해결책으로 제시되는 것은 가상 함수를 부르는 대신 관련한 정보를 파라미터로 받아서 처리하는 것이다. 위의 경우 예를 들면 loadData()를 일반 멤버함수로 작성하고 DataLoader를 파라미터로 넘겨주는 방법을 고려해 볼수 있다.

#include <iostream>
using namespace std;

class DataLoader {
public:
  DataLoader(int v) { _val = v; }
  int getValue() { return _val; }

private:
  int _val;
};

class Data {
public:
  Data(DataLoader &ldr) { loadData(ldr); }
  void dispValue() { cout << "Value is " << _val << endl; }

protected:
  bool loadData(DataLoader &ldr) {
    cout << "Load data " << ldr.getValue() << endl;
    _val = ldr.getValue();
    return true;
  }
  int _val = 0;
};

class DerivedData : public Data {
public:
  DerivedData(DataLoader &ldr) : Data(ldr) {}
};

int main(void) {
  auto l = DataLoader(100);
  auto d = DerivedData(l);
  d.dispValue();
}
$ ./a.out 
Load data 100
Value is 100

VSCode에서 GTest 테스트 항목 표시하기

VSCode의 Test Explorer에 test case들을 표시하면 귀찮은 파라미터 입력 없이도 특정 테스트케이스를 실행하거나 디버그할 수 있어서 여러모로 편리한 점이 많다. 하지만 GTest로 작성한 C++ 테스트케이스들은 자동으로 discover되지 않아서 약간의 설정을 해주어야 한다. 복잡한 것은 아니고 아래와 같이 GTest에서 제공하는 CMake 함수인 gtest_discover_tests()를 이용하면 된다.

# Allow ctest to discover unittests.
enable_testing()
include(GoogleTest)
gtest_discover_tests(
    ${TEST_EXE}
    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
)

gtest_discover_tests() 자세한 내용은 GoogleTest CMake 문서에서 찾을 수 있으니 참고 하도록 하자. 여기에서는 target을 실행파일의 이름으로, working directory를 build로 설정해 주었다. 전체 코드는 아래와 같다.