C++ 고급 문법 (14) — 형변환(Conversion)

send(1024)가 컴파일되고 1024바이트짜리 버퍼가 전송된다. send는 Buffer를 받는 함수인데, Buffer에 크기를 받는 생성자가 있어서 컴파일러가 임시 객체를 만들어 넘긴 것이다. C++은 생성자와 변환 연산자를 통해 사용자 타입 사이에도 암시적 변환(implicit conversion)을 허용한다. 편리하지만 의도하지 않은 변환이 조용히 끼어드는 통로가 되기도 한다. 이 글에서는 g++ 13.3으로 변환 생성자와 explicit, 변환 연산자와 explicit operator bool, 사용자 정의 변환 횟수 제한, 축소 변환, 부호 있는/없는 정수 비교를 확인한다. 명시적 캐스트 연산자는 명시적 캐스팅 편에서 다뤘다.

이 시리즈의 다른 글

변환 생성자와 explicit

인수 하나로 호출할 수 있는 생성자는 그 타입에서 클래스로 가는 암시적 변환으로도 쓰인다.

#include <cstdio>
#include <vector>

class Buffer {
    std::vector<char> data_;
public:
    Buffer(std::size_t n) : data_(n) {}           // 크기를 받는 생성자 (explicit 없음)
    std::size_t size() const { return data_.size(); }
};

void send(const Buffer& b) { std::printf("send %zu bytes\n", b.size()); }

int main()
{
    Buffer b(3);
    send(b);
    send(1024);          // int -> Buffer 임시 객체가 조용히 생긴다
    Buffer c = 'A';      // char -> size_t -> Buffer: 65바이트 버퍼
    std::printf("c.size=%zu\n", c.size());
}
$ g++ -std=c++20 -Wall -o ic implicit_ctor.cpp
$ ./ic
send 3 bytes
send 1024 bytes
c.size=65

'A'는 정수 65로 승격된 뒤 Buffer(65)가 됐다. 생성자에 explicit을 붙이면 두 줄 모두 에러가 된다.

$ g++ -std=c++20 -Wall -c explicit_ctor.cpp 2>&1 | grep error
explicit_ctor.cpp:17:10: error: invalid initialization of reference of type ‘const Buffer&’ from expression of type ‘int’
explicit_ctor.cpp:18:16: error: conversion from ‘char’ to non-scalar type ‘Buffer’ requested

변환 연산자

#include <cstdio>

class Meters {
    double v_;
public:
    explicit Meters(double v) : v_(v) {}
    operator double() const { return v_; }       // 암시적 변환 연산자
};

class Handle {
    int fd_;
public:
    explicit Handle(int fd) : fd_(fd) {}
    operator bool() const { return fd_ >= 0; }   // 암시적 bool 변환
};

int main()
{
    Meters m(3.5);
    double d = m * 2;                  // Meters -> double
    std::printf("d=%.1f\n", d);

    Handle a(3), b(7);
    std::printf("a == b : %d\n", a == b);        // 둘 다 bool true로 바뀌어 비교된다
    std::printf("a + b  : %d\n", a + b);         // bool -> int 로 더해진다
    int fd = a;                                  // 정수로도 받힌다
    std::printf("fd     : %d\n", fd);
}
$ g++ -std=c++20 -Wall -o co convop.cpp
$ ./co
d=7.0
a == b : 1
a + b  : 2
fd     : 1

암시적 operator bool이 있으면 서로 다른 핸들의 == 비교가 true == true가 되고, 덧셈이나 int 대입까지 통과한다. C++11 explicit operator bool은 if, !, && 같은 조건 문맥(contextual conversion)에서만 변환을 허용한다.

#include <cstdio>

class Handle {
    int fd_;
public:
    explicit Handle(int fd) : fd_(fd) {}
    explicit operator bool() const { return fd_ >= 0; }   // 문맥상 bool 변환에서만 허용
};

int main()
{
    Handle a(3), b(-1);
    if (a)   std::puts("a valid");                         // if, while, !, &&, ?: 는 허용
    if (!b)  std::puts("b invalid");
    bool ok = static_cast<bool>(a);
    std::printf("ok=%d\n", ok);
}
$ g++ -std=c++20 -Wall -o eb explicit_bool.cpp
$ ./eb
a valid
b invalid
ok=1
class Handle {
    int fd_;
public:
    explicit Handle(int fd) : fd_(fd) {}
    explicit operator bool() const { return fd_ >= 0; }
};

int main()
{
    Handle a(3), b(7);
    bool same = a == b;
    int  fd   = a;
    return same + fd;
}
$ g++ -std=c++20 -Wall -c explicit_bool_err.cpp 2>&1 | grep error
explicit_bool_err.cpp:11:19: error: no match for ‘operator==’ (operand types are ‘Handle’ and ‘Handle’)
explicit_bool_err.cpp:12:17: error: cannot convert ‘Handle’ to ‘int’ in initialization

사용자 정의 변환은 한 번까지

#include <string>

struct Name {
    std::string s;
    Name(std::string v) : s(std::move(v)) {}     // std::string -> Name
};

void greet(Name) {}

int main()
{
    greet(std::string("kim"));   // 사용자 정의 변환 1회: OK
    greet("kim");                // const char* -> std::string -> Name: 사용자 정의 변환 2회
}
$ g++ -std=c++20 -Wall -c chain.cpp 2>&1 | grep error
chain.cpp:13:11: error: could not convert ‘(const char*)"kim"’ from ‘const char*’ to ‘Name’

암시적 변환 순서(implicit conversion sequence)에는 사용자 정의 변환이 최대 한 번만 들어간다. 표준 변환은 그 앞뒤에 붙을 수 있다(char → size_t → Buffer는 허용).

변환 순서 구성예
표준 변환int → double, 배열 → 포인터, 포인터 → bool
표준 변환 + 사용자 정의 변환 + 표준 변환char → size_t → Buffer
사용자 정의 변환 2회const char* → std::string → Name (불가)

축소 변환과 중괄호 초기화

#include <cstdint>

int main()
{
    double ratio = 3.7;
    int a = ratio;               // 조용히 3
    int b(ratio);                // 조용히 3
    int c{ratio};                // 중괄호: 축소 변환 금지
    std::uint8_t d{300};         // 상수도 범위를 넘으면 금지
    return a + b + c + d;
}
$ g++ -std=c++20 -Wall -c narrowing.cpp 2>&1 | grep -E 'error|warning'
narrowing.cpp:8:11: warning: narrowing conversion of ‘ratio’ from ‘double’ to ‘int’ [-Wnarrowing]
narrowing.cpp:9:20: error: narrowing conversion of ‘300’ from ‘int’ to ‘uint8_t’ {aka ‘unsigned char’} [-Wnarrowing]
$ g++ -std=c++20 -Wall -pedantic-errors -c narrowing.cpp 2>&1 | grep -E 'error|warning'
narrowing.cpp:8:11: error: narrowing conversion of ‘ratio’ from ‘double’ to ‘int’ [-Wnarrowing]
narrowing.cpp:9:20: error: narrowing conversion of ‘300’ from ‘int’ to ‘uint8_t’ {aka ‘unsigned char’} [-Wnarrowing]

표준상 중괄호 안의 축소 변환(narrowing conversion)은 컴파일 에러지만, g++는 상수가 아닌 경우 경고만 낸다. -pedantic-errors를 주면 표준대로 에러가 된다.

부호 있는 정수와 없는 정수

#include <cstdio>
#include <vector>

int main()
{
    int offset = -1;
    unsigned limit = 10;
    if (offset < limit) std::puts("-1 < 10u");      // offset이 unsigned로 변환되어 4294967295
    else                std::puts("-1 >= 10u ?!");

    std::vector<int> v;
    for (int i = 0; i < v.size() - 1; ++i) {         // 0 - 1 = SIZE_MAX
        std::printf("loop body i=%d\n", i);
        if (i == 2) break;
    }
}
$ g++ -std=c++20 -Wall -Wextra -o sg signcmp.cpp 2>&1 | grep -E 'warning'
signcmp.cpp:8:16: warning: comparison of integer expressions of different signedness: ‘int’ and ‘unsigned int’ [-Wsign-compare]
signcmp.cpp:12:23: warning: comparison of integer expressions of different signedness: ‘int’ and ‘std::vector<int>::size_type’ {aka ‘long unsigned int’} [-Wsign-compare]
$ ./sg
-1 >= 10u ?!
loop body i=0
loop body i=1
loop body i=2

int와 unsigned를 비교하면 int가 unsigned로 바뀌어 -1이 4294967295가 된다. 빈 벡터의 size() - 1도 최댓값이 되어 반복문이 끝나지 않는다(예제는 break로 멈췄다).

주의사항

상황문제대응
인수 하나짜리 생성자의도하지 않은 암시적 변환기본으로 explicit, 변환이 자연스러울 때만 생략
암시적 operator bool==, 산술, 정수 대입이 통과explicit operator bool
암시적 수치 변환 연산자단위가 다른 값과 섞여 계산됨explicit 또는 이름 있는 함수(value())
(), = 초기화실수 → 정수 축소가 조용히 일어남중괄호 초기화 + -pedantic-errors
부호 혼합 비교음수가 큰 양수로 바뀜-Wall -Wextra의 -Wsign-compare, C++20 std::cmp_less
size() - 1빈 컨테이너에서 언더플로i + 1 < size()로 쓰거나 C++20 std::ssize()

마무리

  • 인수 하나로 부를 수 있는 생성자와 변환 연산자는 암시적 변환 경로가 되므로 기본으로 explicit을 붙인다.
  • 암시적 변환에는 사용자 정의 변환이 한 번만 들어간다.
  • 중괄호 초기화는 축소 변환을 막고, 부호 혼합 비교는 -Wsign-compare로 잡는다.

참고

답글 남기기