C++ explicit Keyword | "explicit 키워드" 가이드

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_caststatic_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++98C++11C++20
생성자
변환 연산자
조건부 explicitexplicit(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 = 10String(10) 생성컴파일 에러
String s(10)String(10) 생성String(10) 생성
return 10String(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:

관련 글: 타입 변환, 복사 초기화, 스마트 포인터.

한 줄 요약: 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++ 초기화 리스트 생성자 |