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
이 생략된 상태에서는 이 코드가 정의되지 않은 동작을 초래할 수 있습니다.