주간 팁 #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"
을 출력합니다.
그러나 이 코드를 보고 각 인수가 어떤 매개변수에 대응되는지 이해하기 어렵습니다. 예를 들어, precision
과 thousands_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
가 남아있는 상황이 발생합니다. 이를 해결하는 두 가지 방법은 다음과 같습니다:
prefix
타입 변경:string_view
대신std::string
을 사용합니다.- 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;
};
결론
- 여러 매개변수를 받아야 하는 함수에는 옵션 구조체를 사용하여 코드의 가독성과 유지보수성을 높이세요.
- 옵션 구조체를 사용하는 함수 호출에는 지정 초기화자를 활용해 코드 간결성과 안정성을 높이세요.
- 지정 초기화자는 간결성과 명확성 덕분에 옵션 구조체를 사용하는 함수 설계에 유리합니다.