C++ explicit Keyword | "explicit 키워드" 가이드
이 글의 핵심
explicit은 생성자·변환 연산자에 붙여 암시적 변환을 막는 키워드입니다. 복사 초기화 = expr에서 의도치 않은 변환이 일어나지 않게 할 때 쓰고, 스마트 포인터 생성자도 대부분 explicit입니다.
explicit이란?
explicit은 생성자·변환 연산자에 붙여 암시적 변환을 막는 키워드입니다. 복사 초기화 = expr에서 의도치 않은 변환이 일어나지 않게 할 때 쓰고, 스마트 포인터 생성자도 대부분 explicit입니다.
explicit 유무 비교
| 구분 | explicit 없음 | explicit 있음 |
|---|---|---|
| 복사 초기화 | String s = 10; ✅ | String s = 10; ❌ |
| 직접 초기화 | String s(10); ✅ | String s(10); ✅ |
| 중괄호 초기화 | String s{10}; ✅ | String s{10}; ✅ |
| 함수 인자 | func(10); ✅ | func(10); ❌ |
| 반환 값 | return 10; ✅ | return 10; ❌ |
| static_cast | static_cast<String>(10) ✅ | static_cast<String>(10) ✅ |
class String {
public:
String(int size) {} // 암시적 변환 허용
};
String s = 10; // OK: int -> String
// explicit 사용
class String {
public:
explicit String(int size) {}
};
// String s = 10; // 에러
String s(10); // OK: 명시적 생성
변환 과정 다이어그램
graph LR
A[int 값] -->|explicit 없음| B[암시적 변환]
B --> C[생성자 호출]
C --> D[객체 생성]
A -->|explicit 있음| E{초기화 방식}
E -->|복사 초기화 =| F[컴파일 에러]
E -->|직접 초기화 | G[생성자 호출]
G --> D
생성자에 explicit
문제 상황
graph TD
A[함수 호출: process 10] --> B{생성자 explicit?}
B -->|없음| C[암시적 변환 허용]
C --> D[Array 10 임시 객체 생성]
D --> E[함수 실행]
E --> F[의도하지 않은 동작]
B -->|있음| G[컴파일 에러]
G --> H[명시적 변환 필요]
H --> I[process Array 10]
I --> J[의도 명확]
class Array {
public:
// ❌ 암시적 변환 허용
Array(int size) {}
};
void process(Array arr) {}
int main() {
process(10); // int -> Array (의도하지 않음)
}
// ✅ explicit 사용
class Array {
public:
explicit Array(int size) {}
};
// process(10); // 에러
process(Array(10)); // OK
실전 예시
예시 1: 기본 사용
class Vector {
double* data;
size_t size;
public:
explicit Vector(size_t s) : size(s) {
data = new double[size];
}
~Vector() {
delete[] data;
}
};
void process(Vector v) {}
int main() {
// process(10); // 에러
process(Vector(10)); // OK
}
예시 2: 변환 연산자
class Fraction {
int numerator, denominator;
public:
Fraction(int n, int d) : numerator(n), denominator(d) {}
// ❌ 암시적 변환
operator double() const {
return (double)numerator / denominator;
}
};
Fraction f(1, 2);
double d = f; // 암시적 변환
// ✅ explicit 변환 연산자
class Fraction {
public:
explicit operator double() const {
return (double)numerator / denominator;
}
};
// double d = f; // 에러
double d = static_cast<double>(f); // OK
변환 연산자 동작 흐름:
sequenceDiagram
participant Code
participant Compiler
participant Op as Operator
Code->>Compiler: double d = f;
alt no explicit
Compiler->>Op: implicit call
Op->>Code: return double
Note over Code: OK
else with explicit
Compiler->>Compiler: block implicit
Compiler->>Code: compile error
end
Code->>Compiler: static_cast
Compiler->>Op: explicit call
Op->>Code: return double
Note over Code: always OK
예시 3: bool 변환
class SmartPointer {
int* ptr;
public:
explicit SmartPointer(int* p) : ptr(p) {}
// ✅ explicit bool
explicit operator bool() const {
return ptr != nullptr;
}
};
int main() {
SmartPointer sp(new int(10));
if (sp) { // OK: 조건문에서 허용
std::cout << "유효" << std::endl;
}
// bool b = sp; // 에러
bool b = static_cast<bool>(sp); // OK
}
예시 4: 복사 생성자
class Widget {
public:
Widget() = default;
// explicit 복사 생성자 (드물음)
explicit Widget(const Widget& other) {
// ...
}
};
Widget w1;
// Widget w2 = w1; // 에러
Widget w2(w1); // OK
C++11 explicit 확장
C++ 버전별 explicit 지원
| 기능 | C++98 | C++11 | C++20 |
|---|---|---|---|
| 생성자 | ✅ | ✅ | ✅ |
| 변환 연산자 | ❌ | ✅ | ✅ |
| 조건부 explicit | ❌ | ❌ | ✅ explicit(bool) |
| 다중 인자 생성자 | ❌ | ✅ (중괄호 초기화) | ✅ |
// C++11: 변환 연산자에도 explicit
class MyClass {
public:
explicit operator int() const {
return 42;
}
};
MyClass obj;
// int x = obj; // 에러
int x = static_cast<int>(obj); // OK
C++20 조건부 explicit
template<typename T>
class Optional {
public:
// 조건부 explicit
template<typename U>
explicit(!std::is_convertible_v<U, T>)
Optional(U&& value) : data(std::forward<U>(value)) {}
private:
T data;
};
// int -> long은 변환 가능 → explicit 아님
Optional<long> opt1 = 10; // OK
// string -> int는 변환 불가 → explicit
// Optional<int> opt2 = std::string("10"); // 에러
자주 발생하는 문제
문제 1: 의도하지 않은 변환
graph LR
A[process 10 호출] --> B{String int 생성자}
B -->|explicit 없음| C[int → String 암시적 변환]
C --> D[임시 String 10 생성]
D --> E[함수 실행]
E --> F[⚠️ 버그 발생 가능]
B -->|explicit 있음| G[❌ 컴파일 에러]
G --> H[개발자가 의도 명확히 표현]
H --> I[process String 10]
I --> J[✅ 안전한 코드]
// ❌ explicit 없음
class String {
public:
String(int size) {}
};
void process(String s) {}
process(10); // 의도하지 않은 변환
// ✅ explicit
class String {
public:
explicit String(int size) {}
};
실무 사례:
| 시나리오 | explicit 없을 때 | explicit 있을 때 |
|---|---|---|
process(10) | String(10) 생성 후 전달 | 컴파일 에러 |
String s = 10 | String(10) 생성 | 컴파일 에러 |
String s(10) | String(10) 생성 | String(10) 생성 |
return 10 | String(10) 반환 | 컴파일 에러 |
문제 2: 복사 초기화
class Widget {
public:
explicit Widget(int x) {}
};
// Widget w = 10; // 에러
Widget w(10); // OK
Widget w{10}; // OK (C++11)
문제 3: 함수 인자
void func(std::vector<int> vec) {}
// ❌ explicit 없으면
// func(10); // int -> vector (의도하지 않음)
// std::vector 생성자는 explicit
func(std::vector<int>(10)); // OK
문제 4: bool 변환
class Pointer {
public:
operator bool() const { // explicit 없음
return ptr != nullptr;
}
private:
int* ptr;
};
Pointer p;
int x = p; // bool로 변환 후 int로 (의도하지 않음)
// ✅ explicit
explicit operator bool() const {}
bool 변환 문제 다이어그램:
graph TD
A[Pointer p] --> B{operator bool explicit?}
B -->|없음| C[int x = p]
C --> D[Pointer → bool]
D --> E[bool → int]
E --> F[⚠️ x = 0 or 1]
F --> G[의도하지 않은 정수 변환]
B -->|있음| H[int x = p]
H --> I[❌ 컴파일 에러]
I --> J[if p 는 OK]
I --> K[명시적 캐스트 필요]
허용되는 bool 변환 컨텍스트:
| 컨텍스트 | explicit bool | 설명 |
|---|---|---|
if (obj) | ✅ 허용 | 조건문은 명시적 변환 |
while (obj) | ✅ 허용 | 반복문 조건 |
obj && x | ✅ 허용 | 논리 연산자 |
!obj | ✅ 허용 | 논리 NOT |
bool b = obj | ❌ 에러 | 복사 초기화 금지 |
int x = obj | ❌ 에러 | 정수 변환 금지 |
사용 권장사항
explicit 사용 결정 플로우
graph TD
A[생성자/변환 연산자 작성] --> B{단일 인자 생성자?}
B -->|예| C{의도된 암시적 변환?}
B -->|아니오| D{변환 연산자?}
C -->|아니오| E[explicit 사용 ✅]
C -->|예| F[explicit 생략 가능]
D -->|예| G{bool 변환?}
D -->|아니오| H[explicit 불필요]
G -->|예| E
G -->|아니오| C
코드 가이드라인
// ✅ explicit 사용 권장
// 1. 단일 인자 생성자
explicit MyClass(int x);
// 2. 변환 연산자
explicit operator int() const;
// 3. bool 변환 연산자 (필수)
explicit operator bool() const;
// ❌ explicit 불필요
// 1. 다중 인자 생성자
MyClass(int x, int y); // 암시적 변환 안됨
// 2. 복사/이동 생성자
MyClass(const MyClass&); // explicit 드물음
// 3. 기본 생성자
MyClass(); // 인자 없음
실무 체크리스트
| 상황 | explicit 사용 | 이유 |
|---|---|---|
Vector(size_t size) | ✅ 필수 | 의도하지 않은 크기 변환 방지 |
String(const char*) | ❌ 생략 가능 | 문자열 리터럴 변환은 자연스러움 |
operator bool() | ✅ 필수 | 정수 변환 방지 |
operator int() | ✅ 권장 | 의도하지 않은 산술 연산 방지 |
Widget(int, int) | ❌ 불필요 | 다중 인자는 암시적 변환 안됨 |
unique_ptr(T* ptr) | ✅ 필수 | 포인터 자동 변환 방지 |
실무 패턴
패턴 1: 스마트 포인터
template<typename T>
class UniquePtr {
T* ptr_;
public:
explicit UniquePtr(T* ptr = nullptr) : ptr_(ptr) {}
~UniquePtr() { delete ptr_; }
explicit operator bool() const { return ptr_ != nullptr; }
T& operator*() const { return *ptr_; }
T* operator->() const { return ptr_; }
};
// 사용
UniquePtr<int> ptr(new int(42));
if (ptr) { // OK: 조건문
std::cout << *ptr << '\n';
}
// bool b = ptr; // 에러: 복사 초기화
패턴 2: 타입 안전 래퍼
class UserId {
int id_;
public:
explicit UserId(int id) : id_(id) {}
int value() const { return id_; }
};
class OrderId {
int id_;
public:
explicit OrderId(int id) : id_(id) {}
int value() const { return id_; }
};
void processUser(UserId uid) {}
void processOrder(OrderId oid) {}
int main() {
UserId uid(123);
OrderId oid(456);
processUser(uid); // OK
// processUser(oid); // 에러: 타입 불일치
// processUser(123); // 에러: explicit
}
패턴 3: 단위 타입
class Meters {
double value_;
public:
explicit Meters(double v) : value_(v) {}
double value() const { return value_; }
};
class Kilometers {
double value_;
public:
explicit Kilometers(double v) : value_(v) {}
explicit operator Meters() const {
return Meters(value_ * 1000);
}
};
void setDistance(Meters m) {}
int main() {
Kilometers km(5.0);
// setDistance(km); // 에러: 암시적 변환 불가
setDistance(static_cast<Meters>(km)); // OK: 명시적
// setDistance(5.0); // 에러: double → Meters 불가
}
FAQ
Q1: explicit은 언제 사용하나요?
A:
- 단일 인자 생성자 (의도하지 않은 타입 변환 방지)
- 변환 연산자 (특히
bool) - 타입 안전성이 중요한 래퍼 클래스
Q2: 성능 영향은?
A: 없습니다. explicit은 컴파일 타임 검사이므로 런타임 성능에 영향을 주지 않습니다.
Q3: 복사 생성자에도 explicit을 사용하나요?
A: 드뭅니다. 복사 생성자에 explicit을 사용하면 복사 초기화가 불가능해져 불편합니다. 특별한 이유가 있을 때만 사용합니다.
Q4: C++11에서 어떤 변화가 있었나요?
A: 변환 연산자에도 explicit을 사용할 수 있게 되었습니다.
explicit operator int() const; // C++11
Q5: bool 변환 연산자는 항상 explicit인가요?
A: 권장됩니다. explicit operator bool()은 조건문에서는 사용 가능하지만, 정수로의 암묵적 변환을 막습니다.
Q6: C++20 조건부 explicit은 무엇인가요?
A: explicit(bool)로 컴파일 타임 조건에 따라 explicit 여부를 결정할 수 있습니다.
template<typename T, typename U>
explicit(!std::is_convertible_v<U, T>)
MyClass(U&& value);
Q7: 다중 인자 생성자는 explicit이 필요한가요?
A: 일반적으로 불필요합니다. 다중 인자 생성자는 암시적 변환이 발생하지 않습니다. 하지만 C++11 중괄호 초기화에서는 가능하므로 필요 시 사용합니다.
Q8: explicit 학습 리소스는?
A:
- “Effective C++” by Scott Meyers (Item 15)
- “C++ Primer” by Lippman, Lajoie, Moo
- cppreference.com - explicit specifier
관련 글: 타입 변환, 복사 초기화, 스마트 포인터.
한 줄 요약: explicit은 생성자와 변환 연산자의 암시적 변환을 방지하여 타입 안전성을 높입니다.
관련 글: 타입 변환, 복사 초기화, 스마트 포인터.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 타입 변환 | “Type Conversion” 가이드
- C++ Copy Initialization | “복사 초기화” 가이드
- C++ 복사/이동 생성자 | “Rule of Five” 가이드
- C++ 스마트 포인터 | unique_ptr/shared_ptr “메모리 안전” 가이드
관련 글
- C++ 타입 변환 |
- C++ Copy Initialization |
- C++ default와 delete |
- C++ explicit 키워드 |
- C++ 초기화 리스트 생성자 |