send(1024)가 컴파일되고 1024바이트짜리 버퍼가 전송된다. send는 Buffer를 받는 함수인데, Buffer에 크기를 받는 생성자가 있어서 컴파일러가 임시 객체를 만들어 넘긴 것이다. C++은 생성자와 변환 연산자를 통해 사용자 타입 사이에도 암시적 변환(implicit conversion)을 허용한다. 편리하지만 의도하지 않은 변환이 조용히 끼어드는 통로가 되기도 한다. 이 글에서는 g++ 13.3으로 변환 생성자와 explicit, 변환 연산자와 explicit operator bool, 사용자 정의 변환 횟수 제한, 축소 변환, 부호 있는/없는 정수 비교를 확인한다. 명시적 캐스트 연산자는 명시적 캐스팅 편에서 다뤘다.
이 시리즈의 다른 글
- C++ 고급 문법 (12) — cout과 endl
- C++ 고급 문법 (13) — 스마트 포인터(Smart Pointer)
- C++ 고급 문법 (15) — new 연산자(new Expression)
- C++ 고급 문법 (16) — protected 생성자(protected Constructor)
변환 생성자와 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로 잡는다.