Abseil Tip 142 다중 매개변수 생성자와 explicit


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이 필요한 경우 등이 있습니다. 요컨대, 변환 전후의 값이 본질적으로 동일하다면 이러한 기능은 유용합니다.

// 직교 좌표계에서의 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 매개변수로 사용할 때 명시적으로 해당 타입을 생성하도록 요구하여 코드 가독성을 높이고, 의도치 않은 변환으로 인한 버그를 방지할 수 있습니다.

// 직선은 두 점으로 정의됩니다.
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 객체를 생성하지 않고는 기울기를 계산하는 함수에 무관한 점을 전달할 수 없습니다.


추천 사항

  • 복사 생성자와 이동 생성자는 절대 explicit으로 지정하지 마십시오.
  • 인수가 새로 생성된 객체의 “값”인 경우를 제외하고는 생성자를 explicit으로 만드십시오. (참고: Google 스타일 가이드에서는 모든 단일 인수 생성자를 explicit으로 지정할 것을 요구합니다.)
  • 특히, 객체의 정체성(identity, 주소)이 값과 관련된 타입의 생성자는 explicit으로 지정해야 합니다.
  • 값에 추가 제약 조건을 부과하는 생성자(즉, 전제 조건이 있는 경우)는 explicit으로 지정해야 합니다. 이러한 경우 팩토리 함수로 구현하는 것이 더 나을 수 있습니다(Tip #42: 초기화 메서드 대신 팩토리 함수 사용하기).

마무리

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

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

std::vector<char> bad = {"hello", "world"};

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