C++ Static Polymorphism

haeryong·2024년 5월 25일
0

센서퓨전과 같이 여러 종의 센서를 여러 개씩 사용하는 경우 다형성을 활용해 데이터를 일괄적으로 처리해 간단한 구조를 설계할 수 있다.

다만, 클래스의 상속을 이용해 동적 다형성을 구현하는 경우 vtable을 참조해 가상함수를 호출하는 오버헤드 등으로 성능이 저하된다.

template과 C++17에서 추가된 std::variant와 std::visit을 활용해서 효율적인 정적 다형성을 구현해볼 수 있다.

CRTP 패턴

CRTP(Curiously Recurring Template Pattern) 패턴은 다음과 같이 Derived class가 템플릿 Base class의 템플릿 인자로 자기 자신이 들어가는 신기한 형태를 가진다.

template <class _Derived>
class Base{
public:
	void process(){
    	static_cast<_Derived*>(this)->processImpl();
    }
protected:
	virtual void processImpl() = 0;
};

class Derived : public Base<Derived>{
public:
	
protected:
	void processImpl() override {
    	std::cout << "Derived::processImpl()\n";
    }
};

static_cast를 통해 Derived 클래스의 함수를 호출하므로 가상함수 호출 오버헤드가 발생하지 않게 된다.

이제 RadarData, LidarData를 받아 각각 Radar, Lidar 센서에 넣고, process를 수행하는 예시를 작성해본다.

#include <iostream>
#include <map>
#include <variant>
#include <vector>

struct Data {
  int sensor_id;
  double timestamp;
};

struct LidarData : public Data {
  double distance;
};

struct RadarData : public Data {
  double velocity;
};

template <class SensorType, typename DataType>
class Sensor {
public:
  void setSensorId(int sensor_id) { sensor_id_ = sensor_id; }
  int getSensorId() { return sensor_id_; }

  void pushData(DataType &&data) {
    data_.push_back(std::move(data));
    std::cout << "Data pushed\n";
    std::cout << "Data Type is " << typeid(data).name() << "\n";
  }

  DataType getData() { return data_.back(); }

  void process() { static_cast<SensorType *>(this)->processImpl(); }

protected:
  virtual void processImpl() = 0;

  std::vector<DataType> data_;
  int sensor_id_;
};

class LidarSensor : public Sensor<LidarSensor, LidarData> {
public:
  friend Sensor<LidarSensor, LidarData>;
  using DataType = LidarData;

protected:
  void processImpl() override {
    std::cout << "LidarSensor::processImpl() called\n";
  }
};

class RadarSensor : public Sensor<RadarSensor, RadarData> {
public:
  friend Sensor<RadarSensor, RadarData>;
  using DataType = RadarData;

protected:
  void processImpl() override {
    std::cout << "RadarSensor::processImpl() called\n";
  }
};
int main()
{
	LidarSensor lidar;
    RadarSensor radar;
    LidarData lidar_data;
    RadarData radar_data;
    
    lidar.pushData(lidar_data);
    radar.pushData(radar_data);
    
    lidar.process();
    radar.process();
    
}

결과 출력 메시지는 다음과 같다.
동일한 메소드를 이용해서 다양한 타입의 센서를 처리할 수 있다.

Data pushed
Data Type is 9LidarData
Data pushed
Data Type is 9RadarData
LidarSensor::processImpl() called
RadarSensor::processImpl() called

std::variant / std::visit

variant, visit는 C++17에서 추가된 기능으로 정적 다형성을 구현하는 데 필요하다.

다음 코드와 같이 여러 종류의 센서를 하나의 컨테이너에 모두 담아두고 for loop를 이용해 일괄적으로 process를 수행할 수 있다.

std::visit은 variant에 저장된 값을 사용할 수 있도록 도와주는 함수이다. 아래 예시와 같이 variant의 각 타입에 따라 어떠한 행동을 할 것인지 정의하는 함수 객체를 필요로 한다.

process()의 경우 센서 종류에 상관없이 동일하므로 하나의 람다 함수로 쉽게 호출할 수 있었다.

int main() {
  LidarSensor lidar;
  lidar.setSensorId(1);

  RadarSensor radar;
  radar.setSensorId(2);

  std::map<int, std::variant<LidarSensor, RadarSensor>> sensors;
  sensors[lidar.getSensorId()] = lidar;
  sensors[radar.getSensorId()] = radar;

  for (auto &[sensor_id, sensor] : sensors) {
  std::visit([](auto &sensor) { sensor.process(); }, sensor);
  }
  
}
LidarSensor::processImpl() called
RadarSensor::processImpl() called

pushData의 경우 조금 복잡한데, process처럼 구현할 경우 RadarData가 LidarSensor에 pushData되는 경우나
LidarData가 RadarSensor에 PushData되는 경우에 대해 컴파일 에러가 발생한다.

visitor pattern

LidarSensor, RadarSensor 클래스에서 DataType을 각각 using을 이용해 선언해주고, 이를 이용해 DataType과 push되는 Data의 타입이 같은 지 확인하는 로직을 visitor 함수 객체에 추가해주면 컴파엘 에러가 사라지게 된다.

struct SensorPushDataVisitor {
  template <typename SensorType, typename DataType>
  void operator()(SensorType &sensor, DataType &&data) const {
    using SensorDataType = typename SensorType::DataType;
    if constexpr (std::is_same<DataType, SensorDataType>::value) {
      sensor.pushData(std::move(data));
    } else {
      std::cout << "Data type mismatch\n";
    }
  }
};

int main() {
  LidarSensor lidar;
  lidar.setSensorId(1);

  RadarSensor radar;
  radar.setSensorId(2);

  LidarData lidar_data;
  lidar_data.sensor_id = 1;

  RadarData radar_data;
  radar_data.sensor_id = 2;

  std::vector<std::variant<LidarData, RadarData>> data;
  data.push_back(lidar_data);
  data.push_back(radar_data);

  std::map<int, std::variant<LidarSensor, RadarSensor>> sensors;
  sensors[lidar.getSensorId()] = lidar;
  sensors[radar.getSensorId()] = radar;

  for (auto &&datum : data) {
    int sensor_id =
      std::visit([](auto &datum) { return datum.sensor_id; }, datum);
    std::visit(SensorPushDataVisitor{}, sensors[sensor_id], std::move(datum));
  }

  for (auto &[sensor_id, sensor] : sensors) {
    std::visit([](auto &sensor) { sensor.process(); }, sensor);
  }
}

최종 결과 메시지는 다음과 같다.
센서들을 하나의 컨테이너로 관리하고, 하나의 컨테이너에 담긴 여러 종류의 센서 데이터를 각각의 센서에 push한 뒤 일괄적으로 process를 호출할 수 있다.

Data pushed
Data Type is 9LidarData
Data pushed
Data Type is 9RadarData
LidarSensor::processImpl() called
RadarSensor::processImpl() called

0개의 댓글