/

핵심 요약

C++ explicit은 생성자, 변환 함수(C++11~), 추론 가이드(C++17~)가 암시적 변환과 복사 초기화에 사용되지 못하도록 막는 지정자입니다. explicit이 없는 생성자는 변환 생성자로 동작하므로 f(5)처럼 원시 값이 의도치 않게 객체로 변환되어 전달될 수 있습니다. 따라서 단일 인자 생성자는 explicit을 먼저 고려하고, std::string s = "abc";처럼 변환이 자연스러운 경우에만 생략하는 것이 기본 원칙입니다. 조건문에서만 검사를 허용하는 explicit operator bool()(C++11)과 조건부 explicit(bool)(C++20)도 같은 원리의 확장입니다.

이 글에서는 g++ 14.2와 clang 19.1.7로 직접 컴파일한 결과를 바탕으로 explicit이 무엇을 막고 무엇을 허용하는지 정리해 드립니다. 암시적 변환 규칙 자체는 C++ 정수 승격 규칙 글을 참고해 주시기 바랍니다.

C++ explicit이란? 변환 생성자는 무엇인가?

면접형 한 줄 답: explicit은 생성자나 변환 함수가 암시적 변환과 복사 초기화에 쓰이지 못하게 막는 지정자이며, explicit 없이 선언된 생성자가 변환 생성자입니다.

  1. 공식 정의: cppreference에서는 explicit을 "Specifies that a constructor or conversion function (since C++11) or deduction guide (since C++17) is explicit, that is, it cannot be used for implicit conversions and copy-initialization."라고 설명합니다.
  2. 변환 생성자: cppreference의 변환 생성자 문서에 따르면 explicit 없이 선언된 생성자가 변환 생성자입니다. C++11 이전에는 기본값이 없는 단일 매개변수를 갖는 생성자를 가리켰으나, C++11부터는 매개변수가 여러 개인 non-explicit 생성자도 복사 리스트 초기화에 참여합니다. 예를 들어 A a4 = {4, 5};는 허용되지만, 생성자가 explicit으로 선언된 경우 B b4 = {4, 5};는 컴파일 에러가 발생합니다.
  3. explicit 생성자일 때의 동작: B b1 = 1;과 같은 암시적 변환 및 복사 초기화는 컴파일 에러가 발생합니다. 반면 B b2(2);, B b3{4, 5};와 같은 직접 초기화나 명시적 캐스트 (B)1은 문제없이 컴파일됩니다. 또한 explicit operator bool이 정의되어 있다면 if (b2)처럼 조건식 맥락에서는 유효하지만, bool nb1 = b2;처럼 암시적으로 변환할 때는 에러가 발생하며, static_cast<bool>(b2)처럼 명시적으로 캐스팅해야 정상 동작합니다.

explicit을 붙이지 않으면 어떤 버그가 생기나?

면접형 한 줄 답: 객체를 직접 생성하지 않았는데도 int 같은 원시 값이 임시 객체로 조용히 변환되어 함수에 전달되며, 컴파일 경고조차 발생하지 않을 수 있습니다.

  1. 실제 버그 재현 (b.cpp): 아래는 non-explicit 생성자를 사용한 예제입니다. g++ 14.2 -std=c++20 -Wall -Wextra 환경에서 경고 없이 컴파일되고 정상 실행되었습니다.
    #include <cstdio>
    struct Buffer { Buffer(int size) : size(size) {} int size; };
    void resize(Buffer b) { std::printf("Buffer size = %d\n", b.size); }
    int main() { int count = 3; resize(count); }
    실행 결과는 다음과 같습니다.
    Buffer size = 3
    코드 어디에서도 Buffer 객체를 명시적으로 생성하지 않았지만, int 타입 변수인 count가 Buffer로 조용히 변환되어 전달되었습니다.
  2. 표준 라이브러리의 설계: std::vector<int> v = 10;이 컴파일되지 않는 이유도 이와 같습니다. 크기를 인자로 받는 vector 생성자가 explicit으로 선언되어 있어 정수 하나로 원소 10개짜리 컨테이너가 의도치 않게 암시적으로 만들어지는 것을 방지합니다.
  3. 실제 컴파일 결과: g++ (Debian 14.2.0-19) 14.2.0과 Debian clang 19.1.7에서 -std=c++20 -fsyntax-only 옵션으로 컴파일한 a.cpp입니다.
    #include <memory>
    #include <vector>
    struct Meters { explicit Meters(int v) : value(v) {} int value; };
    void f(Meters m) {}
    int main() {
        Meters a{5};      // OK: direct-initialization
        f(Meters{5});     // OK
        Meters b = 5;     // error
        f(5);             // error
        std::vector<int> v = 10;  // error
        std::unique_ptr<int> p = std::make_unique<int>(1);
        if (p) {}         // OK: explicit operator bool in a condition
        bool ok = p;      // error
    }
    g++ 14.2의 에러 출력입니다.
    a.cpp:8:16: error: conversion from ‘int’ to non-scalar type ‘Meters’ requested
    a.cpp:9:7: error: could not convert ‘5’ from ‘int’ to ‘Meters’
    a.cpp:10:26: error: conversion from ‘int’ to non-scalar type ‘std::vector<int>’ requested
    a.cpp:13:15: error: cannot convert ‘std::unique_ptr<int>’ to ‘bool’ in initialization
    clang 19.1.7의 출력(첫 에러와 관련 노트 포함)입니다.
    a.cpp:8:12: error: no viable conversion from 'int' to 'Meters'
    a.cpp:3:26: note: explicit constructor is not a candidate
    a.cpp:9:5: error: no matching function for call to 'f'
    a.cpp:10:22: error: no viable conversion from 'int' to 'std::vector<int>'
  4. 컴파일러가 실제로 어떻게 변환하는지 확인하려면: C++ Insights 활용법 글에서 컴파일러가 임시 객체를 생성하는 과정을 직접 확인하실 수 있습니다.

explicit은 언제 붙이고 언제 빼야 하나?

면접형 한 줄 답: 단일 인자 생성자는 기본적으로 explicit을 먼저 고려하고, std::string s = "abc";처럼 변환이 개념적으로 자연스러울 때만 생략합니다.

  1. 기본 원칙: 단일 인자 생성자는 원칙적으로 explicit 지정을 우선적으로 고려합니다.
  2. 생략해도 되는 경우: std::string의 const char* 생성자는 non-explicit으로 선언되어 있어 std::string s = "abc";와 같은 자연스러운 변환이 가능합니다.
  3. explicit operator bool(): if (p), while (p)와 같은 조건문 맥락에서의 평가만 허용하고, bool ok = p;나 int x = p;와 같은 의도치 않은 암시적 변환은 방지합니다. 명시적 변환이 필요할 때는 static_cast<bool>(p)를 사용하실 수 있습니다. 위 a.cpp의 std::unique_ptr가 대표적인 예입니다.
  4. C++20 조건부 explicit(bool): explicit(expression)은 컴파일 타임 상수 bool 표현식이 true일 때만 explicit으로 동작하며, 기능 테스트 매크로는 __cpp_conditional_explicit(201806L)입니다. 아래는 g++ 14.2 -std=c++20으로 컴파일한 c.cpp입니다.
    template <class T> struct Wrap {
        template <class U> explicit(!std::is_convertible_v<U, T>) Wrap(U u) : t(u) {}
        T t;
    };
    struct S { explicit S(int) {} };
    int main() { Wrap<long> a = 1; Wrap<S> b = 1; }
    실제 컴파일 에러는 다음 한 줄뿐이었습니다.
    c.cpp:7:44: error: conversion from ‘int’ to non-scalar type ‘Wrap<S>’ requested
    int는 long으로 암시적 변환이 가능하므로 조건식이 false가 되어 explicit(false)로 평가되며, 그 결과 Wrap<long> a = 1;은 문제없이 컴파일됩니다. 반면 S의 생성자는 explicit이어서 int에서 S로의 암시적 변환이 불가능하므로 조건식이 true가 되어 explicit(true)로 동작하며, 오직 Wrap<S> b = 1;만 컴파일 에러로 차단됩니다.

생성자 자체의 기본 개념은 C++ 복사 생성자와 이동 생성자 기본 글에 자세히 정리되어 있습니다.

면접에서 explicit은 어떻게 답하나?

면접형 한 줄 답: 아래 네 가지 핵심 질문을 정리해 두시면 explicit 관련 기술 면접 질문에 충분히 대비하실 수 있습니다.

Q1. C++에서 explicit 키워드의 역할과 사용하는 주된 이유는 무엇인가요?

explicit은 생성자, 변환 함수(C++11~), 추론 가이드(C++17~)가 암시적 변환과 복사 초기화에 사용되지 못하도록 막는 지정자입니다. 원시 값이나 다른 타입이 의도치 않게 객체로 변환되어 함수 호출이나 대입 연산에서 버그를 유발하는 것을 방지하고, 직접 초기화(Meters m{5};)나 명시적 캐스트 사용을 유도하기 위해 사용합니다.

Q2. explicit 생성자를 가진 클래스에서 허용되는 초기화와 차단되는 초기화의 차이는 무엇인가요?

B b = 1;, B b = {4, 5};, f(5)와 같은 복사 초기화 및 암시적 인자 전달은 컴파일 에러가 발생합니다. 반면 B b(2);, B b{4, 5};와 같은 직접 초기화와 (B)1, static_cast<B>(1)과 같은 명시적 캐스트는 정상적으로 허용됩니다.

Q3. 스마트 포인터나 래퍼 클래스에서 explicit operator bool()을 선언하는 이유는 무엇인가요?

if (ptr), while (ptr)처럼 조건문 맥락에서의 유효성 검사는 허용하면서도, bool ok = ptr;나 int x = ptr;처럼 의도치 않은 암시적 타입 변환이 일어나는 것을 방지하기 위해서입니다.

Q4. 단일 인자 생성자에서 explicit을 생략해도 되는 경우는 언제인가요?

std::string s = "abc";처럼 타입 변환이 개념적으로 자연스럽고 안전할 때입니다. 실제로 std::string의 const char* 생성자는 non-explicit으로 선언되어 있습니다. 다만 일반적인 개발 원칙으로는 단일 인자 생성자에 explicit 지정을 우선적으로 고려해야 합니다.

+ Recent posts