C++ explicit 키워드 | "암시적 변환 방지" 생성자 에러 해결
이 글의 핵심
C++ explicit 키워드에 대한 실전 가이드입니다.
들어가며: “함수에 잘못된 타입을 전달했는데 컴파일이 돼요"
"의도하지 않은 타입 변환이 일어나요”
C++에서 단일 인자 생성자는 암시적 타입 변환을 허용합니다. 이는 의도하지 않은 버그를 유발할 수 있습니다.
// ❌ 암시적 변환 허용
class String {
char* data_;
size_t size_;
public:
String(size_t size) : data_(new char[size]), size_(size) {
std::cout << "String(" << size << ") 생성\n";
}
~String() {
delete[] data_;
}
};
void process(String s) {
// ...
}
int main() {
process(100); // ❌ int → String 암시적 변환!
// String(100) 생성 → process 호출 → 소멸
// 의도: 100개 문자 처리?
// 실제: 100바이트 String 객체 생성
}
이 글에서 다루는 것:
- explicit 키워드란?
- 암시적 변환의 문제점
- explicit 사용 규칙
- 실전 패턴
목차
1. 암시적 변환의 문제점
문제 1: 잘못된 타입 전달
// ❌ 암시적 변환
class Array {
int* data_;
size_t size_;
public:
Array(size_t size) : data_(new int[size]), size_(size) {
std::cout << "Array(" << size << ") 생성\n";
}
~Array() {
delete[] data_;
}
};
void processArray(Array arr) {
// ...
}
int main() {
processArray(10); // ❌ int → Array 암시적 변환
// 의도: 10개 요소 배열?
// 실제: 크기 10인 Array 객체 생성
Array arr = 20; // ❌ 복사 초기화도 가능
}
문제 2: 성능 저하
// ❌ 불필요한 객체 생성
class BigData {
std::vector<int> data_;
public:
BigData(size_t size) : data_(size, 0) {
std::cout << "BigData(" << size << ") 생성 (비용 큼)\n";
}
};
void process(BigData data) {
// ...
}
int main() {
for (int i = 0; i < 1000; ++i) {
process(1000000); // ❌ 매번 BigData 객체 생성!
}
}
문제 3: 의도하지 않은 비교
// ❌ 잘못된 비교
class Fraction {
int numerator_;
int denominator_;
public:
Fraction(int num, int den = 1)
: numerator_(num), denominator_(den) {}
bool operator==(const Fraction& other) const {
return numerator_ * other.denominator_ ==
other.numerator_ * denominator_;
}
};
int main() {
Fraction f(1, 2); // 1/2
if (f == 0) { // ❌ 0 → Fraction(0, 1) 암시적 변환
// 의도: f가 0인지 확인?
// 실제: f == Fraction(0, 1) 비교
}
}
2. explicit 키워드
explicit 생성자
// ✅ explicit 생성자
class Array {
int* data_;
size_t size_;
public:
explicit Array(size_t size) : data_(new int[size]), size_(size) {
std::cout << "Array(" << size << ") 생성\n";
}
~Array() {
delete[] data_;
}
};
void processArray(Array arr) {
// ...
}
int main() {
// processArray(10); // ❌ 컴파일 에러
// error: could not convert '10' from 'int' to 'Array'
processArray(Array(10)); // ✅ 명시적 변환
// Array arr = 20; // ❌ 컴파일 에러
Array arr(20); // ✅ 직접 초기화
Array arr2{20}; // ✅ 유니폼 초기화
}
explicit 변환 연산자 (C++11)
// ✅ explicit 변환 연산자
class SmartPointer {
int* ptr_;
public:
SmartPointer(int* p) : ptr_(p) {}
// bool로의 암시적 변환 방지
explicit operator bool() const {
return ptr_ != nullptr;
}
int& operator*() const {
return *ptr_;
}
};
int main() {
SmartPointer ptr(new int(42));
// ✅ if문에서는 사용 가능
if (ptr) {
std::cout << *ptr << '\n';
}
// ❌ bool 변수에 암시적 대입 불가
// bool b = ptr; // 컴파일 에러
// ✅ 명시적 변환
bool b = static_cast<bool>(ptr);
// ❌ 산술 연산 불가
// int x = ptr + 1; // 컴파일 에러
}
3. explicit 사용 규칙
규칙 1: 단일 인자 생성자는 거의 항상 explicit
// ✅ explicit 사용
class String {
public:
explicit String(size_t size);
explicit String(const char* str);
};
class Vector {
public:
explicit Vector(size_t size);
};
class File {
public:
explicit File(const std::string& path);
};
규칙 2: 다중 인자 생성자는 선택적
// 다중 인자 생성자
class Point {
public:
// explicit 불필요 (암시적 변환 가능성 낮음)
Point(int x, int y) : x_(x), y_(y) {}
private:
int x_, y_;
};
int main() {
// Point p = 10; // 컴파일 에러 (인자 부족)
Point p(10, 20); // OK
}
규칙 3: 변환 의도가 명확하면 explicit 생략
// explicit 생략 (변환 의도가 명확)
class Complex {
double real_, imag_;
public:
// 실수 → 복소수 변환은 자연스러움
Complex(double real, double imag = 0.0)
: real_(real), imag_(imag) {}
};
void process(Complex c) {
// ...
}
int main() {
process(3.14); // ✅ double → Complex 암시적 변환 (의도적)
Complex c = 2.0; // ✅ 복사 초기화 (의도적)
}
4. 실전 패턴
패턴 1: 스마트 포인터
// ✅ std::unique_ptr 스타일
template <typename T>
class UniquePtr {
T* ptr_;
public:
explicit UniquePtr(T* ptr = nullptr) : ptr_(ptr) {}
~UniquePtr() {
delete ptr_;
}
// bool 변환 (explicit)
explicit operator bool() const {
return ptr_ != nullptr;
}
T& operator*() const {
return *ptr_;
}
T* operator->() const {
return ptr_;
}
// 복사 금지
UniquePtr(const UniquePtr&) = delete;
UniquePtr& operator=(const UniquePtr&) = delete;
};
int main() {
UniquePtr<int> ptr(new int(42));
if (ptr) { // ✅ explicit operator bool
std::cout << *ptr << '\n';
}
// bool b = ptr; // ❌ 컴파일 에러
}
패턴 2: RAII 래퍼
// ✅ 파일 핸들 래퍼
class File {
FILE* file_;
public:
explicit File(const char* path, const char* mode = "r") {
file_ = fopen(path, mode);
if (!file_) {
throw std::runtime_error("파일 열기 실패");
}
}
~File() {
if (file_) {
fclose(file_);
}
}
explicit operator bool() const {
return file_ != nullptr;
}
FILE* get() const {
return file_;
}
// 복사 금지
File(const File&) = delete;
File& operator=(const File&) = delete;
};
int main() {
File file("data.txt");
if (file) {
// 파일 읽기
char buffer[1024];
fread(buffer, 1, sizeof(buffer), file.get());
}
}
패턴 3: 강타입 (Strong Type)
// ✅ 강타입 패턴
class Meters {
double value_;
public:
explicit Meters(double value) : value_(value) {}
double value() const { return value_; }
Meters operator+(const Meters& other) const {
return Meters(value_ + other.value_);
}
};
class Feet {
double value_;
public:
explicit Feet(double value) : value_(value) {}
double value() const { return value_; }
Feet operator+(const Feet& other) const {
return Feet(value_ + other.value_);
}
};
void setDistance(Meters m) {
std::cout << "거리: " << m.value() << "m\n";
}
int main() {
Meters m(10.0);
Feet f(30.0);
setDistance(m); // ✅ OK
// setDistance(f); // ❌ 컴파일 에러 (타입 안전성)
// setDistance(10.0); // ❌ 컴파일 에러
// Meters total = m + f; // ❌ 컴파일 에러 (단위 혼용 방지)
}
정리
explicit 사용 가이드
| 상황 | explicit | 이유 |
|---|---|---|
| 단일 인자 생성자 | ✅ 권장 | 암시적 변환 방지 |
| 다중 인자 생성자 | 선택적 | 암시적 변환 가능성 낮음 |
| 변환 연산자 | ✅ 권장 | 의도하지 않은 변환 방지 |
| 자연스러운 변환 | ❌ | double → Complex 등 |
핵심 규칙
- 단일 인자 생성자는 거의 항상 explicit
- 변환 연산자도 explicit 고려
- 의도적 변환은 explicit 생략
- 타입 안전성 우선
체크리스트
- 단일 인자 생성자에 explicit을 사용하는가?
- 변환 연산자에 explicit을 고려하는가?
- 암시적 변환이 의도된 것인가?
- 타입 안전성을 확보했는가?
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 클래스 기초 | class 완벽 가이드
- C++ 생성자 | Constructor 가이드
- C++ 타입 변환 | Type Conversion
- C++ 강타입 패턴 | Strong Type
자주 하는 실수
실수 1: explicit 누락
// ❌ 흔한 실수: explicit 없음
class String {
public:
String(size_t size) { /* ... */ } // ❌ explicit 없음
};
void process(String s) { /* ... */ }
int main() {
process(100); // ❌ 의도하지 않은 변환
// String(100) 생성 → process 호출
}
// ✅ 올바른 구현
class String {
public:
explicit String(size_t size) { /* ... */ }
};
// process(100); // 컴파일 에러
process(String(100)); // 명시적 변환 필요
실수 2: 복사 초기화
// ❌ 실수: 복사 초기화
class Array {
public:
Array(size_t size) { /* ... */ } // explicit 없음
};
Array arr = 10; // ❌ 암시적 변환
// ✅ explicit 사용
class Array {
public:
explicit Array(size_t size) { /* ... */ }
};
// Array arr = 10; // 컴파일 에러
Array arr(10); // OK
Array arr{10}; // OK
실수 3: 함수 오버로딩
// ❌ 실수: 오버로딩 혼란
class Value {
public:
Value(int x) { /* ... */ } // explicit 없음
Value(double x) { /* ... */ } // explicit 없음
};
void process(Value v) { /* ... */ }
process(42); // int → Value(int)
process(3.14); // double → Value(double)
process('A'); // ❌ 모호함! char → int? double?
실무 트러블슈팅
문제: 컴파일은 되는데 의도와 다른 동작
증상:
class Size {
public:
Size(int bytes) : bytes_(bytes) {}
private:
int bytes_;
};
void allocate(Size size) {
// size.bytes_만큼 할당
}
int main() {
allocate(1024); // ✅ 컴파일 성공
// 하지만 의도: 1024바이트? 1024KB? 1024MB?
}
해결:
class Size {
public:
explicit Size(int bytes) : bytes_(bytes) {}
static Size bytes(int n) { return Size(n); }
static Size kilobytes(int n) { return Size(n * 1024); }
static Size megabytes(int n) { return Size(n * 1024 * 1024); }
};
// allocate(1024); // 컴파일 에러
allocate(Size::kilobytes(1024)); // 명확!
문제: 템플릿 인자 추론 실패
증상:
template <typename T>
void process(std::vector<T> vec) { /* ... */ }
// process({1, 2, 3}); // 컴파일 에러
원인: explicit 생성자로 인한 추론 실패
해결:
process(std::vector<int>{1, 2, 3}); // 명시적 타입
성능 영향 분석
explicit의 성능 영향
| 항목 | explicit 없음 | explicit 있음 |
|---|---|---|
| 컴파일 시간 | 동일 | 동일 |
| 런타임 성능 | 동일 | 동일 |
| 바이너리 크기 | 동일 | 동일 |
| 타입 안전성 | ❌ 낮음 | ✅ 높음 |
// explicit은 컴파일 타임에만 영향
// 런타임 오버헤드 없음!
베스트 프랙티스
1. 단일 인자 생성자 체크리스트
class MyClass {
public:
// ✅ 항상 explicit 고려
explicit MyClass(int value);
explicit MyClass(const std::string& str);
explicit MyClass(size_t size);
// ❌ 의도적 변환만 explicit 생략
MyClass(double real, double imag = 0.0); // 복소수
};
2. 변환 연산자 가이드
class SmartPointer {
public:
// ✅ bool 변환은 explicit
explicit operator bool() const {
return ptr_ != nullptr;
}
// ❌ 다른 타입 변환은 신중히
// operator int() const { return *ptr_; } // 위험!
};
3. 코드 리뷰 체크포인트
// 🔍 리뷰 시 확인사항
class Widget {
public:
Widget(int id); // ⚠️ explicit 필요?
Widget(const std::string& name); // ⚠️ explicit 필요?
Widget(int x, int y); // ✅ 다중 인자 - explicit 선택적
};
실무 시나리오
시나리오 1: 단위 타입 (Unit Types)
// ✅ 실무 예시: 물리 단위
class Meters {
double value_;
public:
explicit Meters(double value) : value_(value) {}
double value() const { return value_; }
};
class Feet {
double value_;
public:
explicit Feet(double value) : value_(value) {}
double value() const { return value_; }
// 명시적 변환만 허용
explicit Feet(Meters m) : value_(m.value() * 3.28084) {}
};
void setHeight(Meters m) {
std::cout << "높이: " << m.value() << "m\n";
}
int main() {
// setHeight(10.0); // 컴파일 에러 - 단위 명시 필요
setHeight(Meters(10.0)); // OK
Meters m(10.0);
// Feet f = m; // 컴파일 에러 - 명시적 변환 필요
Feet f(m); // OK
}
시나리오 2: 리소스 핸들
// ✅ 실무 예시: 파일 핸들
class FileHandle {
int fd_;
public:
explicit FileHandle(const char* path) {
fd_ = open(path, O_RDONLY);
if (fd_ < 0) {
throw std::runtime_error("파일 열기 실패");
}
}
~FileHandle() {
if (fd_ >= 0) {
close(fd_);
}
}
explicit operator bool() const {
return fd_ >= 0;
}
int get() const { return fd_; }
};
int main() {
// FileHandle file = "data.txt"; // 컴파일 에러
FileHandle file("data.txt"); // OK
if (file) { // explicit operator bool
read(file.get(), buffer, size);
}
}
시나리오 3: 스마트 포인터 래퍼
// ✅ 실무 예시: 커스텀 스마트 포인터
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(const UniquePtr&) = delete;
UniquePtr& operator=(const UniquePtr&) = delete;
};
int main() {
// UniquePtr<int> ptr = new int(42); // 컴파일 에러
UniquePtr<int> ptr(new int(42)); // OK
if (ptr) { // explicit operator bool
std::cout << *ptr << '\n';
}
}
Clang-Tidy 규칙
explicit 체크 자동화
# .clang-tidy
Checks: 'google-explicit-constructor'
# 단일 인자 생성자에 explicit 강제
// 경고 예시
class MyClass {
public:
MyClass(int value); // warning: single-argument constructors must be marked explicit
};
마치며
explicit 키워드는 암시적 타입 변환을 방지해 타입 안전성을 높입니다.
핵심 원칙:
- 단일 인자 생성자는 거의 항상 explicit
- 변환 연산자도 explicit 고려
- 타입 안전성 우선
실무 팁:
- Clang-Tidy로 자동 체크
- 코드 리뷰 시 단일 인자 생성자 확인
- 단위 타입에는 반드시 explicit 사용
단일 인자 생성자에는 explicit을 습관화하세요.
다음 단계: explicit을 이해했다면, C++ 타입 변환 가이드에서 더 깊이 배워보세요.
관련 글
- C++ 시리즈 전체 보기
- C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
- C++ ADL |
- C++ Aggregate Initialization |