title: “Tip of the Week #142: 다중 매개변수 생성자와 explicit” layout: tips sidenav: side-nav-tips.html published: true permalink: tips/142 type: markdown order: “142”

처음 게시: 2018-01-29 (TotW #142)

작성자: James Dennett

업데이트: 2020-04-06

바로가기: abseil.io/tips/142

“명시적(explicit)이 암시적(implicit)보다 낫다.” –
PEP 20


요약:

대부분의 생성자는 explicit이어야 합니다.


소개

C++11 이전에는 explicit 키워드가 단일 인수로 호출될 수 있는 생성자에만 의미가 있었으며, 이러한 생성자가 “변환 생성자”로 작동하지 않도록 하기 위해 스타일 가이드에서는 explicit을 요구했습니다. 그러나 다중 매개변수 생성자에 대해서는 이 요구가 적용되지 않았습니다. 실제로 C++11 이전에는 다중 매개변수 생성자에 대해 explicit을 사용하는 것이 의미가 없었기 때문에, 이를 권장하지 않았습니다. 하지만 이제는 상황이 달라졌습니다.

C++11부터는 explicit이 중괄호 초기화(braced initialization)에서 의미를 가지게 되었습니다. 예를 들어, void f(std::pair<int, int>)와 같은 함수를 호출할 때 f({1, 2}) 또는 std::vector<char> bad = {"hello", "world"};와 같이 변수를 초기화할 때 해당됩니다.

잠깐! 마지막 예제에서 타입이 맞지 않는데요. 저 코드가 컴파일되나요? std::vector<std::string> good = {"hello", "world"};는 말이 되지만, std::vector<char>는 두 개의 std::string을 담을 수 없습니다. 그런데도 현재의 C++ 컴파일러에서는 컴파일됩니다. 이게 어떻게 가능한 걸까요? 이에 대해 더 이야기하기 전에 explicit에 대해 조금 더 살펴보겠습니다.


값을 변경하지 않는 타입 변환 생성자

explicit으로 표시되지 않은 생성자는 해당 타입 이름을 명시하지 않고도 컴파일러가 호출할 수 있습니다. 이는 우리가 이미 필요한 값을 가지고 있지만, 타입이 약간 일치하지 않을 때 유용합니다. 예를 들어 const char[]를 가지고 있고 std::string이 필요하거나, 두 개의 std::string이 있고 이를 std::vector<std::string>으로 변환하거나, int를 가지고 있는데 BigNum이 필요한 경우 등이 있습니다. 요컨대, 변환 전후의 값이 본질적으로 동일하다면 이러한 기능은 유용합니다.

CPP
// 직교 좌표계에서의 2D 좌표를 나타냄
class Coordinate2D {
 public:
  Coordinate2D(double x, double y);
  // ...
};

// 주어진 점 `p`의 유클리드 거리 계산
double EuclideanNorm(Coordinate2D p);

// `explicit`이 없는 생성자의 사용 예:
double norm = EuclideanNorm({3.0, 4.0});  // 함수 인수 전달
Coordinate2D origin = {0.0, 0.0};         // `=` 초기화
Coordinate2D Translate(Coordinate2D p, Vector2D v) {
  return {p.x() + v.x(), p.y() + v.y()};  // 함수 반환값
}
클릭하여 더 보기

Coordinate2D(double, double) 생성자를 explicit으로 선언하지 않음으로써, 함수에 Coordinate2D를 전달할 때 {3.0, 4.0}과 같은 값을 사용할 수 있습니다. 이러한 값이 해당 객체에 대해 완전히 합리적이라면, 이를 허용해도 혼란을 초래하지 않습니다.


추가 작업이 필요한 생성자

생성자가 암시적으로 호출되면 입력값과 다른 결과를 출력하거나 전제 조건(precondition)이 있는 경우 문제가 될 수 있습니다.

예를 들어 Request 클래스와 Request(Server*, Connection*) 생성자를 생각해봅시다. 요청 객체의 값이 서버와 연결을 “의미”하는 것은 아닙니다. 단지 이를 사용하여 요청 객체를 생성할 수 있을 뿐입니다. {server, connection}에서 생성될 수 있는 의미적으로 다른 타입(예: Response)도 있을 수 있습니다. 이러한 생성자는 explicit으로 표시해야, {server, connection}Request 또는 Response 매개변수로 사용할 때 명시적으로 해당 타입을 생성하도록 요구하여 코드 가독성을 높이고, 의도치 않은 변환으로 인한 버그를 방지할 수 있습니다.

CPP
// 직선은 두 점으로 정의됩니다.
class Line {
 public:
  // 주어진 두 점을 지나는 직선 생성
  // REQUIRES: p1 != p2
  explicit Line(Coordinate2D p1, Coordinate2D p2);

  // 이 직선이 특정 점 `p`를 포함하는지 확인
  bool ContainsPoint(Coordinate2D p) const;
};

Line line({0, 0}, {42, 1729});

// 직선 `line`의 기울기 계산 (수직이면 무한 반환)
double Gradient(const Line& line);
클릭하여 더 보기

Line(Coordinate2D, Coordinate2D) 생성자를 explicit으로 선언하면, 명시적으로 Line 객체를 생성하지 않고는 기울기를 계산하는 함수에 무관한 점을 전달할 수 없습니다.


추천 사항


마무리

이 팁은 Tip #88: =, (), 그리고 {} 초기화와 반대되는 관점으로 볼 수 있습니다. Tip #88은 “의도된 리터럴 값”으로 초기화할 때 복사 초기화(=) 문법을 사용하라고 권장합니다. 이번 팁에서는 Tip #88에서 복사 초기화를 사용하라고 조언하는 경우를 제외하고 explicit을 사용하는 것이 좋다고 권장합니다.

마지막으로 주의 사항: C++ 표준 라이브러리는 항상 올바르게 작동하지는 않습니다. 예를 들어, 다음 코드에서는 std::vector<char> 대신 문자열 컨테이너를 사용해야 하지만, 잘못된 코드가 여전히 컴파일됩니다:

CPP
std::vector<char> bad = {"hello", "world"};
클릭하여 더 보기

이 코드는 std::vector의 템플릿 “범위” 생성자가 쌍의 반복자를 허용하고, 여기서 반복자 유형을 const char*로 추론하기 때문에 컴파일됩니다. 그러나 이 생성자가 explicit이었다면, 잘못된 타입을 사용한 코드는 오류로 표시되었을 것입니다. explicit이 생략된 상태에서는 이 코드가 정의되지 않은 동작을 초래할 수 있습니다.

라이선스

저작자: Jaehun Ryu

링크: https://jaehun.me/posts/abseil-tip-142-%EB%8B%A4%EC%A4%91-%EB%A7%A4%EA%B0%9C%EB%B3%80%EC%88%98-%EC%83%9D%EC%84%B1%EC%9E%90%EC%99%80-explicit/

라이선스: CC BY 4.0

이 저작물은 크리에이티브 커먼즈 저작자표시 4.0 국제 라이선스에 따라 이용할 수 있습니다. 출처를 밝히면 상업적 목적을 포함해 자유롭게 이용 가능합니다.

댓글

검색 시작

검색어를 입력하세요

↑↓
ESC
⌘K 단축키