C++ Error Handling Revisited - Raphael Meyer 보면서 정리

C++ 에서 에러 처리를 어떻게 할수 있는지에 대해 알아보는 영상을 보면서 정리(?) 한 내용입니다.

영상에서는 haskell 예제를 보여주며 functional 컨셉을 설명하지만, (영어가 짧아 이해가 되지 않아) 단순히 변경되어가는 코드만 정리했습니다. 몇가지 코드(방법)이 더 있었는데 코드 자체가 이해가 안되서 정리하지 않았습니다.

C/C++ 에서 일반적인 예외 처리는 다음과 같습니다.

bool MoistureSensor::read(Moisture &moisture) { /* ... */ }
bool Pump::pump(Volume amount) { /* ... */ }
bool WateringSystem::water(Volume& amount) {
  Moisture moisture;
  if (moisture_sensor.read(moisture)) {
    Temperature temperature;
    if (thermo_sensor.read(temperature)) {
      if (calculate_amount(moisture, temperatue, amount)) {
        if (pump.pump(amount)) {
          return true;
        }
      }
    }
  }
  return false;
}

이를 좀더 보기 편하게 정리하면 다음과 같이 쓸 수 있습니다.

bool MoistureSensor::read(Moisture& moisture) { /* ... */ }
bool Pump::pump(Volume amount) { /* ... */ }
bool WateringSystem::water(Volume& amount) {
  Moisture moisture;
  if (not moisture_sensor.read(moisture)) {
    return false;
  }
  Temperature temperature;
  if (not thermo_sensor.read(temperature)) {
    return false;
  }
  if (not calculate_amount(moisture, temperature, amount)) {
    return false;
  }
  if (not pump.pump(amount)) {
    return false;
  }
  return true;
}

C++17 에 포함된 std::optional 을 쓰면 다음과 같이 할 수 있습니다.

std::optional<Moisture> MoistureSensor::read() { /* ... */ }
std::optional<std::monostate> Pump::pump(Volume amount) { /* ... */ }
std::optional<Volume> WateringSystem::water() {
  auto const moisture = moisture_sensor.read();
  if (not moisture) {
    return std::nullopt;
  }
  auto const temperature = thermo_sensor.read();
  if (not temperature) {
    return std::nullopt;
  }
  auto const amount = calculate_amount(moisture.value(), temperature.value());
  if (not amount) {
    return std::nullopt;
  }
  if (not pump.pump(amount.value())) {
    return std::nullptr;
  }

  return amount;
}

std::variant 을 사용해서 에러 객체를 반환할수 있도록 개선할 수 있습니다.

std::variant<Moisture, Error> MoistureSencor::read() { /* ... */ }
std::variant<std::monostate, Error> Pump::pump(Volume amount) { /* ... */ }
std::variant<Volume, Error> WateringSystem::water() {
  auto const moisture = mosture_sensor.read();
  if (auto const error = std::get_if<Error>(&mositure); error) {
    return *error;
  }
  auto const temperature = thermo_sensor.read();
  if (auto const error = std::get_if<Error>(&temperature); error) {
    return *error;
  }
  auto const amount = calculate_amount(std::get<Moisture>(moisture), std::get<Temperature>(temperature));
  if (auto const error = std::get_if<Error>)(&amount); error) {
    return *error;
  }
  auto const pump_result = pump.pump(std::get<Volume>(amount));
  if (auto const error = std::get_if<Error>(&pump_result); error) {
    return *error;
  }
  return amount;
}

물론 C++ 이 제공하는 예외처리를 사용할 수 있습니다. 하지만, try catch 가 마치 goto 와 같이 느껴져서 좋아하지 않는 사람도 있습니다.

Moisture MoistureSensor::read() { /* ... */ }
void Pump::pump(Volume amount) { /* ... */ }
Volume WateringSystem::water() {
  try {
    auto const moisture = moisture_sensor.read();
    auto const temperature = thermo_sensor.read();
    auto const amount = calculate_amount(moisture, temperature);
    pump.pump(amount);
    return amount;
  }
  catch(SensorError const& e) {
    throw WateringError{e.what()};
  }
  catch(PumpError const& e) {
    throw WateringError{e.what()};
  }
}

RUST (rust-lang) 에서의 방법은 다음과 같습니다.

/*
enum Result<T, E> {
  Ok(T),
  Err(E),
}
*/
fn read_moisture() -> Result<Moisture, String> { /* ... */ }
fn pump(v: &Volume) -> Result<(), String> { /* ... */ }
fn water_plant() -> Result<Volume, String> {
  let moisture = match read_moisture() {
    Ok(m) => m,
    Err(e) => return Err(e),
  };
  let temperature = match read_temperature() {
    Ok(t) => t,
    Err(e) => return Err(e),
  };
  let amount = match calculate_amount(moisture, temperature) {
    Ok(v) => v,
    Err(e) => return Err(e),
  };
  return match pump(&amount) {
    Ok(_) => Ok(amount),
    Err(e) => Err(e),
  };
  /* 다음과 같이 줄여 쓸 수 있음. */
  /*
  let moisture = read_moisture()?;
  let temperature = read_temperature()?;
  let amount = calculate_amount(moisture, temperature)?;
  pump(&amount)?;
  return Ok(amount);
   */
}

C++ 에서 RUST 와 유사한 방법을 사용해보면 다음과 같이 할 수 있습니다.

template<typename OkType, typename ErrType>
class Result
{
  public:
  Result() = default;

  template<typename T, typename std::enable_if<std::is_convertible<T, std::variant<ErrType, OkType>>{}, int>::type = 0>
  Result(T &&value):_value{std::forward<T>(value)} {}

  ErrType const& err() const { return std::get<err_index>(_value); }
  OkType const& ok() const { return std::get<ok_index>(_value); }

  bool is_err() const { result _value.index() == err_index; }
  bool is_ok() const { result _value.index() == ok_index; }

  private:
  constexpr static std::size_t err_index = 0;
  constexpr static std::size_t ok_index = 1;
  std::variant<ErrType, OkType> _value;

  Result(std::variant<ErrType, OkType> value):_value{std::move(value)} {}

  friend auto make_ok<OkType, ErrType>(OkType value);
  friend auto make_err<OkType, ErrType>(ErrType value);
};

template<typename OkType, typename ErrType>
auto make_ok(OkType value) {
  using T = Result<OkType, ErrType>;
  return T{std::variant<ErrType, OkType>{
    std::in_place_index<T::ok_index>,
    std::move(value)
  }};
}

template<typename OkType, typename ErrType>
auto make_err(ErrType value) {
  using T = Result<OkType, ErrType>;
  return T{std::variant<ErrType, OkType>{
    std::in_place_index<T::err_index>,
    std::move(value)
  }};
}

Result<Moisture, Error> MoistureSensor::read() { /* ... */ }
Result<std::monostate, Error> Pump::pump(Volume amount) { /* ... */ }
Result<Volume, Error> WateringSystem::water() {
  auto const moisture = moisture_sensor.read();
  if (moisture.is_error()) {
    return moisture.err();
  }
  auto const temperature = thermo_sensor.read();
  if (temperature.is_err()) {
    return temperature.err();
  }
  auto const amount = calculate_amount(moisture.ok(), temperature.ok());
  if (amount.is_err()) {
    return amount.err();
  }
  auto const pump_result = pump.pump(amount.ok());
  if (pump_result.is_err()) {
    return pump_result.err();
  }
  return amount;
}

int main() {
  auto const amount = WateringSystem{}.water();
  if (amount.is_ok()) {
    std::cout<<"Water "<<amount.ok().ml<<" ml\n";
  } else {
    std::cout<<amount.err().what<<"\n";
  }
}

Use value types to write code that is expressive and clearly communicates its intent. 라고 합니다.


comments powered by Disqus