본문으로 건너뛰기
Embedded C++ for Real Systems · 1/41

임베디드 C++ vs C — 런타임·코드 크기·ABI 관점 비교

· Hawk · 7분 읽기

#한 줄 요약

“Modern C++는 추상화 비용을 거의 0으로 만든다.” 단, 그 zero가 성립하는 전제 조건들을 알아야 합니다.

#어떤 문제를 푸는가

새 임베디드 프로젝트의 언어를 정할 때 가장 먼저 부딪히는 질문입니다. “C로 갈까 C++로 갈까.”

C++를 택하면 다음 두려움이 따라옵니다.

  • 코드 크기가 폭증할 것 같습니다
  • 런타임 비용을 가늠하기 어렵습니다
  • 디버깅이 더 까다로워질 것 같습니다
  • 인증(MISRA, DO-178C)에서 막힐 것 같습니다

이 두려움 중 일부는 1990년대 C++의 잔영입니다. Modern C++(11/14/17/20)는 측정 가능한 zero-cost abstraction을 약속합니다. 다만 모든 기능이 그렇지는 않습니다. 어느 기능이 무료이고 어느 기능이 유료인지를 정확히 아는 것이 핵심입니다.

#C++가 C에 더하는 것

C++는 C의 superset에 가깝지만 동일하지는 않습니다. C++가 더하는 도구는 크게 네 가지입니다.

도구컴파일 타임 비용런타임 비용
클래스 (멤버 함수, public/private)작음0 (call만)
템플릿큼 (bloat 가능)0 (인스턴스마다 코드)
constexpr0 또는 음수(런타임 코드 제거)
RAII (소멸자)작음0 (생성자 인라인)
가상 함수작음vtable 간접 호출 1회
예외 처리있음 (대부분 환경에서 끔)
RTTI (typeid, dynamic_cast)작음있음 (끔)
스마트 포인터작음unique_ptr 0, shared_ptr 있음
iostream큼 (printf 대비 ~50KB)

핵심: C와 동등한 런타임 비용을 가지는 기능이 대부분입니다. 비용을 가지는 것은 예외, RTTI, iostream, 동적 다형성 정도입니다. 이들을 끄거나 사용하지 않으면 C와 같은 영역에서 출발합니다.

한눈에 보면 다음과 같이 정리됩니다.

C++ 기능별 런타임 비용

#측정 — 단순 함수의 어셈블리

말로는 부족합니다. 어셈블리를 직접 봅니다.

같은 일을 하는 두 코드를 비교합니다. ARM Cortex-M4, -O2, GCC 13 환경입니다.

// C 버전
int add_one(int x) {
return x + 1;
}
// C++ 버전
struct Counter {
int value;
Counter(int v) : value(v) {}
int add_one() const { return value + 1; }
};
int caller(int x) {
Counter c(x);
return c.add_one();
}

어셈블리 결과:

# C: add_one(int)
add_one:
adds r0, r0, #1
bx lr
# C++: caller(int)
caller(int):
adds r0, r0, #1
bx lr

완전히 동일합니다. C++의 생성자 호출과 멤버 함수 호출이 컴파일러에 의해 사라졌습니다. 이것이 zero-cost abstraction의 가장 단순한 예입니다.

조금 더 현실적인 예를 봅니다. STM32 보드의 GPIO blink입니다.

// C 버전
#define GPIOA_BSRR (*(volatile uint32_t*)0x40020018)
void blink_on(void) { GPIOA_BSRR = 1 << 5; }
void blink_off(void) { GPIOA_BSRR = 1 << 21; }
// C++ 버전 (RAII + 템플릿)
template<uint32_t Address, uint8_t Pin>
struct Gpio {
static void on() { *reinterpret_cast<volatile uint32_t*>(Address + 0x18) = 1u << Pin; }
static void off() { *reinterpret_cast<volatile uint32_t*>(Address + 0x18) = 1u << (Pin + 16); }
};
using Led = Gpio<0x40020000, 5>;
void blink_on() { Led::on(); }
void blink_off() { Led::off(); }

-O2 어셈블리:

# C: blink_on
blink_on:
ldr r3, =0x40020018
movs r2, #32
str r2, [r3]
bx lr
# C++: blink_on
blink_on:
ldr r3, =0x40020018
movs r2, #32
str r2, [r3]
bx lr

역시 동일합니다. C++의 템플릿은 컴파일 타임 추상화입니다. AddressPin이 상수로 박혀 어셈블리에서는 C 매크로와 같은 결과가 나옵니다.

장점은 타입 안전성입니다. Gpio<0x40020000, 5>Gpio<0x40020400, 12>는 다른 타입이라 서로 섞어 쓸 수 없습니다. C의 매크로로는 불가능한 보호입니다.

#비용이 진짜로 발생하는 영역

zero-cost가 아닌 영역도 있습니다. 이것들을 알고 끄는 것이 임베디드 C++의 절반입니다.

#1. 예외 처리

예외는 unwind table과 런타임 핸들러를 함께 데려옵니다.

// 예외 사용
int divide(int a, int b) {
if (b == 0) throw std::runtime_error("divide by zero");
return a / b;
}

이 함수 하나가 3-10KB의 런타임을 요구합니다. 임베디드는 보통 예외 자체를 끕니다.

CXXFLAGS += -fno-exceptions

대안으로는 std::optional, std::expected (C++23), error code 반환이 있습니다. 자세한 내용은 Part 3-05: No-Exception 설계에서 다룹니다.

#2. RTTI

dynamic_casttypeid는 type info 테이블을 데려옵니다. 보통 수 KB 정도입니다.

CXXFLAGS += -fno-rtti

대안으로는 tagged union, std::variant, visitor 패턴(CRTP 기반)이 있습니다.

#3. iostream

std::cout은 50KB 이상의 코드를 데려옵니다. 임베디드에서는 printf보다 무겁습니다.

대안으로는 printf(C 라이브러리), fmt(header-only, 작음), 직접 UART 출력 등이 있습니다.

#4. 가상 함수

vtable 간접 호출이 1회 발생합니다. 무료는 아니지만 대부분 무시할 수 있습니다. 인라인되지 않는다는 점이 단점입니다.

대안은 CRTP(static polymorphism, Part 2-08)입니다.

#5. std::function

내부적으로 heap 할당과 type erasure를 사용합니다. 임베디드에서는 함수 포인터나 FixedFunction(etl::delegate)을 씁니다.

#코드 크기 비교 — 실측

같은 기능을 C와 C++로 작성했을 때의 실제 크기 비교입니다. STM32F4, -Os -flto -fno-exceptions -fno-rtti 환경입니다.

항목CC++차이
빈 main240 B240 B0
GPIO blink312 B312 B0
UART driver1.2 KB1.2 KB0
Ring buffer480 B488 B+8 B
RTOS task (FreeRTOS)18 KB18 KB0
printf 사용+9 KB+9 KB0
std::cout 사용+52 KB+52 KB
dynamic_cast 1회+3 KB+3 KB

적절히 끄면 C++가 C와 같거나 수 바이트 차이입니다. 기본을 모르고 쓰면 수십 KB의 bloat가 한 번에 들어옵니다.

#자주 보는 함정과 안티패턴

#1. -fno-exceptions 없이 시작

GCC 기본값은 예외 활성입니다. 임베디드 프로젝트는 처음부터 -fno-exceptions를 켜야 합니다. 늦게 끄면 기존 코드가 깨집니다.

#2. std::vector를 무심코 사용

vector는 heap 할당을 합니다. 자유 메모리가 없는 환경에서는 런타임 실패로 이어집니다. std::array, etl::vector(고정 크기)로 대체합니다.

#3. virtual을 모든 곳에 적용

“OOP니까 virtual”은 아닙니다. 진짜 런타임 다형성이 필요한 곳에만 씁니다. 그 외에는 CRTP를 씁니다.

#4. iostream 사용 후 크기 폭증에 놀람

<< 한 번에 50KB가 따라옵니다. printf 또는 fmt를 씁니다.

#5. std::shared_ptr로 모든 소유권 표현

shared_ptr은 atomic 카운터와 heap을 함께 끌고 옵니다. 임베디드는 unique_ptr이 기본이며 raw pointer와 명확한 소유자 지정도 괜찮습니다.

#6. C++ 표준 라이브러리 전부 사용 가능하다고 가정

<filesystem>, <thread>, <chrono>의 일부는 OS 의존입니다. bare-metal에서는 링크 실패로 이어집니다. ETL, EASTL 같은 임베디드 친화 라이브러리를 검토합니다.

#Modern C++가 가져온 변화

C++11 이전과 이후는 거의 다른 언어에 가깝습니다.

기능추가 표준임베디드 가치
constexprC++11런타임 → 컴파일 타임
auto, range-forC++11가독성
nullptrC++11NULL 대체, 타입 안전
enum classC++11네임스페이스 분리
unique_ptrC++11RAII 소유권
static_assertC++11컴파일 타임 검증
noexceptC++11예외 없음 명시
constexpr 확장C++14/17/20더 많은 컴파일 타임
std::arrayC++11고정 크기 컨테이너
Variadic templateC++11가변 인자 (printf 안전 대체)
ConceptsC++20템플릿 제약, 에러 메시지
std::spanC++20포인터 + 길이 안전 wrap
std::expectedC++23예외 없는 에러 처리

이 중 대부분이 zero-cost입니다. 템플릿 기반 + 컴파일 타임 기능들이 핵심입니다.

#C가 여전히 옳은 자리

C++가 만능은 아닙니다. C가 더 적합한 경우는 다음과 같습니다.

  • 부트 코드 / 인터럽트 벡터 테이블: 매우 초기 단계라 C++ 런타임이 초기화되지 않았습니다
  • 극소형 MCU: ATtiny 같은 4KB 미만 Flash 환경에서는 C++ 런타임 자체가 부담입니다
  • 인증 제약: DO-178C Level A 같은 환경에서는 C++ 도구 인증 비용이 큽니다
  • legacy 코드: 수십만 줄의 C 코드베이스에 C++ 일부만 섞으면 복잡성이 커집니다
  • 팀 역량: C++ 숙련도가 낮은 팀에서는 느린 도입이 빠른 도입보다 안전합니다

특히 부트 코드는 거의 항상 C 또는 어셈블리입니다. __libc_init_array가 호출되기 전에는 static 생성자가 돌지 않았기 때문에 C++ 객체를 생성할 수 없습니다. 자세한 내용은 Part 1-06: 스타트업 코드에서 다룹니다.

#C와 C++의 공존 전략

대부분의 임베디드 프로젝트는 C와 C++가 섞여 있습니다.

  • HAL/드라이버: C(벤더 제공, 검증된 코드)
  • 부트/스타트업: C 또는 어셈블리
  • 애플리케이션 로직: C++(RAII, 타입 안전, 추상화 활용)
  • 외부 라이브러리 wrapper: C++(C API를 RAII로 감쌉니다)

C/C++ 혼합 시에는 name mangling과 ABI가 문제가 됩니다. extern "C" 블록과 헤더 분리 패턴이 표준 해법입니다. 자세한 내용은 Part 1-05: ABI 호환성에서 다룹니다.

#핵심 질문 — 언어가 아닌 사용법

C와 C++의 선택은 언어 자체의 우열 문제가 아닙니다. 어떤 기능을 어떻게 쓰는지가 결정합니다.

C++를 안전하게 쓰려면 시리즈 전체를 관통하는 네 질문을 각 결정마다 던집니다.

  1. 이 기능이 런타임 비용을 추가하는가? 어셈블리로 확인합니다
  2. 이 기능이 디버깅을 더 쉽게 만드는가? 타입 안전과 명확한 의도가 핵심입니다
  3. 이 기능이 코드 품질을 끌어올리는가? RAII, const, constexpr이 그 예입니다
  4. 바이너리 크기가 감당 가능한가? 측정과 -Os -flto로 확인합니다

C++의 강점인 추상화가 부담 없이 가능한지를 프로젝트마다 평가합니다.

#정리

  • Modern C++의 대부분은 zero-cost abstraction이며 어셈블리는 C와 동일하게 나옵니다.
  • 비용이 발생하는 영역은 예외, RTTI, iostream, 동적 할당, std::function이며 알고 끄거나 대체합니다.
  • C는 부트 코드, 극소형 MCU, 인증 환경에서 여전히 적합합니다.
  • 결정은 언어 선택이 아니라 사용법 선택입니다. 위 네 가지 핵심 질문으로 매 결정을 평가합니다.

#관련 항목

#다음 글

Part 1-02: 컴파일러 플래그 가이드 — C++를 임베디드 모드로 만드는 정확한 플래그 조합과 그것이 어셈블리에 미치는 영향을 다룹니다.

Embedded C++ for Real Systems · 2 of 41

  1. 1Embedded C++ for Real Systems — 임베디드 모던 C++ 시리즈 소개
  2. 2임베디드 C++ vs C — 런타임·코드 크기·ABI 관점 비교
  3. 3임베디드 C++ 컴파일러 플래그 분석 — -fno-rtti·-fno-exceptions·-Os
  4. 4임베디드 C++ 런타임 요구사항 — libstdc++·newlib·crt0 분석
  5. 5C++ 코드 크기 분석 — 가상 함수·템플릿·예외 비용 추적
  6. 6C++ ABI 호환성 — Itanium ABI·name mangling·vtable 레이아웃
  7. 7C++ 스타트업 코드 분석 — .init_array·전역 생성자 호출 순서
  8. 8임베디드 C++ 링커 스크립트 — vtable·정적 객체 배치 추적
  9. 9임베디드 C++ 표준 선택 가이드 — C++11/14/17/20/23 트레이드오프
  10. 10임베디드 RAII 기초 — 리소스 안전성과 결정적 소멸 보장
  11. 11임베디드 RAII 실전 패턴 — Lock·Pin·DMA·Power 관리
  12. 12constexpr 기초와 임베디드 적용 — 컴파일 타임 계산 활용
  13. 13constexpr 고급 활용 — 룩업 테이블·CRC·해시 컴파일 타임 생성
  14. 14consteval과 constinit 분석 — C++20 컴파일 타임 강제 메커니즘
  15. 15임베디드 Templates 기초 — 타입 안전과 코드 재사용 분석
  16. 16Template 비용 분석 — 코드 폭증·인스턴스화·디버그 정보 측정
  17. 17CRTP 패턴 분석 — vtable 없는 정적 다형성
  18. 18Type Traits 임베디드 활용 — SFINAE·is_pod·컴파일 타임 검사
  19. 19C++20 Concepts 활용 — 템플릿 제약과 가독성 개선
  20. 20동적 할당 없는 임베디드 C++ — placement new·정적 객체·풀
  21. 21Custom Allocator 기초 — std::allocator 인터페이스 분석
  22. 22Pool Allocator 구현 — Fixed-Size Block과 O(1) 보장
  23. 23std::pmr 임베디드 활용 — Polymorphic Memory Resource 분석
  24. 24No-Exception C++ 설계 — 코드 크기·결정성 트레이드오프
  25. 25임베디드 에러 처리 패턴 — Result·errno·optional 비교
  26. 26std::expected 분석 — C++23 결과 타입과 에러 전파
  27. 27No-RTTI C++ 설계 — dynamic_cast 제거와 정적 타입 분기
  28. 28임베디드 스마트 포인터 선택 — unique·shared·custom 비교
  29. 29임베디드 C++ 소유권 모델 — single·shared·borrow 패턴
  30. 30Intrusive Containers 분석 — 동적 할당 없는 컨테이너 설계
  31. 31ETL 라이브러리 분석 — Embedded Template Library의 STL 대체
  32. 32임베디드 Lock-free 기초 — atomic·memory ordering·CAS
  33. 33Lock-free Container 구현 — SPSC Queue·Ring Buffer
  34. 34Type-safe Flags 패턴 — Enum Class·Strong Typedef·Tag
  35. 35임베디드 State Machine 패턴 — Variant·Visitor·Table-driven 비교
  36. 36Compile-time FSM 구현 — 템플릿으로 상태 전이 검증
  37. 37Singleton 대안 패턴 — Service Locator·Static Init·Phantom
  38. 38MMIO Register 추상화 — 타입 안전한 비트 필드 접근
  39. 39GPIO 추상화 패턴 — Template·Concept으로 보드 독립성
  40. 40Peripheral 추상화 — UART·SPI·I2C 공통 인터페이스 설계
  41. 41임베디드 HAL 설계 패턴 — Static·Dynamic·Hybrid 비교