Abseil Tip 173 옵션 구조체로 인수 래핑하기

주간 팁 #173: 옵션 구조체로 인수 래핑하기

원래 TotW #173으로 2019년 12월 19일 게시됨
작성자: John Bandela
2020-04-06 업데이트됨

빠른 링크: abseil.io/tips/173


상자는 없었고, 가방도 없었어요. 그는 머리가 아플 때까지 고민했어요.

- 닥터 수스


지정 초기화자

지정 초기화자는 C++20에서 도입된 기능으로, 현재 대부분의 컴파일러에서 사용할 수 있습니다. 지정 초기화자는 옵션 구조체를 더 쉽게, 더 안전하게 사용할 수 있도록 합니다. 옵션 객체를 함수 호출 중에 생성할 수 있기 때문입니다. 이는 코드의 길이를 줄이고, 옵션 구조체와 관련된 임시 객체의 수명 문제를 피할 수 있습니다.

struct PrintDoubleOptions {
  absl::string_view prefix = "";
  int precision = 8;
  char thousands_separator = ',';
  char decimal_separator = '.';
  bool scientific = false;
};

void PrintDouble(double value,
                 const PrintDoubleOptions& options = PrintDoubleOptions{});

std::string name = "my_value";
PrintDouble(5.0, {.prefix = absl::StrCat(name, "="), .scientific = true});

옵션 구조체가 왜 유용하며 지정 초기화자가 이러한 사용에서 발생할 수 있는 잠재적인 문제를 어떻게 해결하는지에 대한 자세한 배경은 계속 읽어보세요.


많은 인수 전달 문제

인수를 많이 받는 함수는 혼란스러울 수 있습니다. 예를 들어, 부동소수점 값을 출력하는 다음 함수가 있다고 가정해봅시다.

void PrintDouble(double value, absl::string_view prefix, int precision,
                 char thousands_separator, char decimal_separator,
                 bool scientific);

이 함수는 많은 옵션을 받아 유연성을 제공합니다.

PrintDouble(5.0, "my_value=", 2, ',', '.', false);

위 코드는 "my_value=5.00"을 출력합니다.

그러나 이 코드를 보고 각 인수가 어떤 매개변수에 대응되는지 이해하기 어렵습니다. 예를 들어, precisionthousands_separator의 순서를 실수로 바꾸면:

PrintDouble(5.0, "my_value=", ',', '.', 2, false);

이런 오류를 방지하기 위해 인수 주석을 사용하는 것이 일반적입니다. 다음과 같이 작성하면 ClangTidy가 오류를 감지할 수 있습니다:

PrintDouble(5.0, "my_value=",
            /*precision=*/2,
            /*thousands_separator=*/',',
            /*decimal_separator=*/'.',
            /*scientific=*/false);

하지만 인수 주석에도 몇 가지 단점이 있습니다:

  • 강제성 부족: ClangTidy 경고는 빌드 시 감지되지 않습니다. 작은 실수(예: = 기호 누락)로 인해 검사 자체가 비활성화될 수 있습니다.
  • 지원 제한: 모든 프로젝트와 플랫폼에서 ClangTidy를 지원하지 않습니다.

또한 많은 옵션을 명시하는 것은 번거로울 수 있습니다. 대부분의 경우 옵션에 대해 합리적인 기본값을 설정하는 것이 더 나은 방법입니다.

void PrintDouble(double value, absl::string_view prefix = "", int precision = 8,
                 char thousands_separator = ',', char decimal_separator = '.',
                 bool scientific = false);

이제 기본값을 활용해 더 간결하게 PrintDouble을 호출할 수 있습니다.

PrintDouble(5.0, "my_value=");

하지만 scientific에 대해 기본값이 아닌 값을 지정하려면 이전 모든 매개변수에 값을 명시해야 합니다:

PrintDouble(5.0, "my_value=",
            /*precision=*/8,              // 기본값 유지
            /*thousands_separator=*/',',  // 기본값 유지
            /*decimal_separator=*/'.',    // 기본값 유지
            /*scientific=*/true);

옵션 구조체 사용

이 문제를 해결하려면 모든 옵션을 옵션 구조체로 그룹화할 수 있습니다:

struct PrintDoubleOptions {
  absl::string_view prefix = "";
  int precision = 8;
  char thousands_separator = ',';
  char decimal_separator = '.';
  bool scientific = false;
};

void PrintDouble(double value,
                 const PrintDoubleOptions& options = PrintDoubleOptions{});

이제 값을 명확하게 지정하고 기본값을 유연하게 사용할 수 있습니다.

PrintDoubleOptions options;
options.prefix = "my_value=";
PrintDouble(5.0, options);

주의사항

옵션 구조체를 사용할 때 몇 가지 주의할 점이 있습니다.

임시 객체의 수명 문제

모든 옵션을 개별 매개변수로 받을 때는 다음 코드가 안전합니다:

std::string name = "my_value";
PrintDouble(5.0, absl::StrCat(name, "="));

그러나 옵션 구조체를 사용할 경우 다음과 같은 코드가 위험합니다:

std::string name = "my_value";
PrintDoubleOptions options;
options.prefix = absl::StrCat(name, "=");
PrintDouble(5.0, options);

위 코드에서는 임시 문자열의 수명이 끝난 뒤 string_view가 남아있는 상황이 발생합니다. 이를 해결하는 두 가지 방법은 다음과 같습니다:

  1. prefix 타입 변경: string_view 대신 std::string을 사용합니다.
  2. Setter 함수 사용: 멤버 변수를 설정하는 함수를 추가합니다.
class PrintDoubleOptions {
 public:
  PrintDoubleOptions& set_prefix(absl::string_view prefix) {
    prefix_ = prefix;
    return *this;
  }

 private:
  std::string prefix_;
  int precision_ = 8;
  char thousands_separator_ = ',';
  char decimal_separator_ = '.';
  bool scientific_ = false;
};

결론

  1. 여러 매개변수를 받아야 하는 함수에는 옵션 구조체를 사용하여 코드의 가독성과 유지보수성을 높이세요.
  2. 옵션 구조체를 사용하는 함수 호출에는 지정 초기화자를 활용해 코드 간결성과 안정성을 높이세요.
  3. 지정 초기화자는 간결성과 명확성 덕분에 옵션 구조체를 사용하는 함수 설계에 유리합니다.