암시적 변환 (Implicit Conversions)¶
암시적 변환을 정의하지 마세요. 변환 연산자와 단일 인수 생성자에는 explicit 키워드를 사용하세요.
정의:
암시적 변환은 어떤 타입(원본 타입이라고 합니다)의 객체를 다른 타입(대상 타입이라고 합니다)이 필요한 자리에 사용할 수 있게 해줍니다. double 매개변수를 받는 함수에 int 인수를 전달하는 경우가 그런 예입니다.
언어가 정의한 암시적 변환 외에도, 사용자는 원본 타입이나 대상 타입의 클래스 정의에 적절한 멤버를 추가해 자신만의 변환을 정의할 수 있습니다. 원본 타입 쪽의 암시적 변환은 대상 타입의 이름을 딴 타입 변환 연산자(예: operator bool())로 정의합니다. 대상 타입 쪽의 암시적 변환은 원본 타입을 유일한 인수로(또는 기본값이 없는 유일한 인수로) 받을 수 있는 생성자로 정의합니다.
explicit 키워드는 생성자나 변환 연산자에 붙일 수 있으며, 사용 지점에서 대상 타입이 명시적으로 드러날 때만(예: 캐스트와 함께) 사용되도록 보장합니다. 이는 암시적 변환뿐 아니라 목록 초기화 문법에도 적용됩니다.
이런 코드는 엄밀히 말해 암시적 변환이 아니지만, explicit에 관한 한 언어는 이를 암시적 변환으로 취급합니다.
장점:
- 암시적 변환은 타입이 분명할 때 그 이름을 명시적으로 적을 필요를 없애 주므로, 타입을 더 쓰기 좋고 표현력 있게 만들 수 있습니다.
- 암시적 변환은 오버로딩의 더 단순한 대안이 될 수 있습니다. 예를 들어
string_view매개변수를 받는 함수 하나가std::string용과const char*용 오버로드를 따로 두는 것을 대신할 수 있습니다. - 목록 초기화 문법은 객체를 간결하고 표현력 있게 초기화하는 방법입니다.
단점:
- 암시적 변환은 타입 불일치 버그를 숨길 수 있습니다. 대상 타입이 사용자의 기대와 다르거나, 사용자가 변환이 일어난다는 사실을 모를 수 있습니다.
- 암시적 변환은 실제로 어떤 코드가 호출되는지 덜 분명하게 만들어, 특히 오버로딩이 있을 때 코드를 읽기 어렵게 만들 수 있습니다.
- 인수를 하나 받는 생성자는 그럴 의도가 없었더라도 실수로 암시적 타입 변환에 쓰일 수 있습니다.
- 단일 인수 생성자에
explicit이 붙어 있지 않으면, 암시적 변환을 정의하려던 것인지 작성자가 표시를 잊은 것인지 확실히 알 방법이 없습니다. - 암시적 변환은 호출 지점의 모호성을 낳을 수 있으며, 특히 양방향 암시적 변환이 있을 때 그렇습니다. 이는 두 타입이 모두 암시적 변환을 제공하거나, 한 타입이 암시적 생성자와 암시적 타입 변환 연산자를 모두 가질 때 생깁니다.
- 대상 타입이 암시적이라면 목록 초기화도 같은 문제를 겪을 수 있으며, 특히 목록에 원소가 하나뿐일 때 그렇습니다.
결정:
타입 변환 연산자와 단일 인수로 호출할 수 있는 생성자는 클래스 정의에서 explicit으로 표시해야 합니다. 예외적으로 복사 생성자와 이동 생성자는 타입 변환을 수행하지 않으므로 explicit이어서는 안 됩니다.
서로 교환 가능하도록 설계된 타입, 예를 들어 두 타입의 객체가 같은 밑바탕 값을 다르게 표현한 것일 때는 암시적 변환이 필요하고 적절할 수 있습니다. 그런 경우에는 프로젝트 리드에게 문의해 이 규칙의 예외를 요청하세요.
단일 인수로 호출할 수 없는 생성자는 explicit을 생략해도 됩니다. std::initializer_list 매개변수 하나만 받는 생성자도 복사 초기화(예: MyType m = {1, 2};)를 지원하기 위해 explicit을 생략해야 합니다.
옮긴이 풀이¶
핵심: 단일 인수 생성자와 변환 연산자에 explicit을 붙여라¶
암시적 변환은 A 타입을 기대하는 자리에 B 타입을 그냥 넣을 수 있게 해줍니다. 편해 보이지만, 어떤 코드가 실제로 호출되는지 불분명해지고 타입 불일치 버그를 숨깁니다.
class Foo {
public:
explicit Foo(int x); // explicit가 없으면 Foo가 필요한 곳에 int가 슬쩍 변환됨
};
void Func(Foo f);
Func(42); // explicit면 컴파일 오류 (변환 의도가 드러나도록 강제)
Func(Foo(42)); // 이렇게 명시적으로 써야 함
규칙¶
- 단일 인수로 호출 가능한 생성자와 변환 연산자(
operator bool()등)는explicit으로 표시하세요. - 이는 중괄호 목록 초기화(
Func({42, 3.14}))에도 적용됩니다.
예외 (explicit를 붙이지 않음)¶
- 복사·이동 생성자: 타입 변환이 아니므로
explicit을 붙이지 않습니다. - 단일 인수로 호출할 수 없는 생성자.
- 단일
std::initializer_list매개변수 생성자: 복사 초기화(MyType m = {1, 2};)를 지원하려면explicit을 생략합니다. - 서로 교환 가능하도록 설계된 타입(같은 값의 다른 표현)은 암시적 변환이 적절할 수 있으니, 이 경우 프로젝트 리드에게 예외를 요청하세요.