C++ explicit 키워드 | "암시적 변환 방지" 생성자 에러 해결

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. 암시적 변환의 문제점
  2. explicit 키워드
  3. explicit 사용 규칙
  4. 실전 패턴
  5. 정리

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 등

핵심 규칙

  1. 단일 인자 생성자는 거의 항상 explicit
  2. 변환 연산자도 explicit 고려
  3. 의도적 변환은 explicit 생략
  4. 타입 안전성 우선

체크리스트

  • 단일 인자 생성자에 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 키워드암시적 타입 변환을 방지타입 안전성을 높입니다.

핵심 원칙:

  1. 단일 인자 생성자는 거의 항상 explicit
  2. 변환 연산자도 explicit 고려
  3. 타입 안전성 우선

실무 팁:

  • Clang-Tidy로 자동 체크
  • 코드 리뷰 시 단일 인자 생성자 확인
  • 단위 타입에는 반드시 explicit 사용

단일 인자 생성자에는 explicit을 습관화하세요.

다음 단계: explicit을 이해했다면, C++ 타입 변환 가이드에서 더 깊이 배워보세요.


관련 글

  • C++ 시리즈 전체 보기
  • C++ Adapter Pattern 완벽 가이드 | 인터페이스 변환과 호환성
  • C++ ADL |
  • C++ Aggregate Initialization |