programming language/C++

[C++ 기초] enum class, union, struct와 variant 비교

공대키메라 2026. 5. 19. 21:30

이 글을 정리하는 이유는 A tour of C++를 읽는 도중 2장 마지막 부분에서 다음과 같은 말이 있기 때문이다.

 

많은 경우 union보다 variant가 사용하는 편이 더 간단하고 안전하다.

2장 사용자 타입 정의 59page - A tour of C++ 중...

 

이번 글에서는 위 문장이 무슨말인지를 이를 이해하기 위해서

c++에서 존재하는 enum class, union과  struct에 대해 이해하고 예시 코드를 볼 것이다.

 

그리고 variant 를 도입해서 어떻게 코드를 깔끔하게 정리할 수 있는지 알아볼 것이다.

 

글의 내용들은 읽어보고 좋다고 생각되는 글에서 많이 참고해왔다.


목표

1. enum class, union, struct를 어떻게 사용하는지 이해한다.

2. variant가 무엇인지, 사용하면 어떻게 좋은지 이해한다.


0.  들어가기 전에

사실 왜 이름이 C++11, C++14이런 식인지도 몰랐다.

 

현재 가이드로 읽고 있는 A tour of C++의 경우 딱히 특정 버전에 국한적으로 알려주지 않고 대부분 C++11 을 기점으로 

기초 설명이 많은것으로 이해했는데, C++뒤에 붙는 숫자는 발표 연도를 그냥 붙인거였다.

C++ 버전 발표 연도 주요 특징
C++98 / C++03 1998/2003 최초의 표준 및 오류 수정
C++11 2011 모던 C++의 시작(람다, auto, 스마트포인터)
C++14 2014 C++11의 버그 수정 및 소규모 확장
C++17 2017 구조적 바인딩(Structured Binding) 등 편의성 강화
C++20 2020 코루틴, 모듈, 캘린더 등 대규모 기능 추가
C++23 2024 라이브러리 확장 및 기능 개선

 

1. enum class사용법과 변천사 알아보기

1.1 Enum class 도입 in C++11

Enum혹은 enumerated type은 개발자가 이름이 붙은 값들 정의하도록 돕는 사용자 정의 타입이다.

 

Enum Class는 스코프드 열거형이라고도 불리며(scoped enumeration) 전통적인 C스타일 enums의 대안으로 타입 안정적이다(type-safe)고 C++11부터 도입이 되었다.

 

그렇다면 C++11이전의 Enum만 사용하게 되면 타입안정적이지 않아 작업에 어려움을 겪었을것으로 보인다.

 

여기서 scoped 라는 뜻이 헷갈렸는데 Java, Javscript에서와 비슷하게 생각한다.

scope는 흔히 범위로 아무래도 하나의 열거형에 제한되도록 했기에 scoped enumeration 으로 명명한것이 아닌가 한다. 

 

다음 코드는 geekforgeeks에서 가져온 예시 코드이다.

(출처 : https://www.geeksforgeeks.org/cpp/enum-classes-in-c-and-their-advantage-over-enum-datatype/)

 

// C++ program to demonstrate working
// of Enum Classes

#include <iostream>
using namespace std;

int main()
{

    enum class Color { Red,
                       Green,
                       Blue };
    enum class Color2 { Red,
                        Black,
                        White };
    enum class People { Good,
                        Bad };

    // An enum value can now be used
    // to create variables
    int Green = 10;

    // Instantiating the Enum Class
    Color x = Color::Green;

    // Comparison now is completely type-safe
    if (x == Color::Red)
        cout << "It's Red\n";
    else
        cout << "It's not Red\n";

    People p = People::Good;

    if (p == People::Bad)
        cout << "Bad people\n";
    else
        cout << "Good people\n";

    // gives an error
    // if(x == p)
    // cout<<"red is equal to good";

    // won't work as there is no
    // implicit conversion to int
    // cout<< x;

    cout << int(x);

    return 0;
}

 

enum class 선언하게 되면 명확하지 않았던 코드가 어드 class에 속하는지 눈에 바로 보이며 동일한 변수명으로 선언한 것 때문에 헷갈리지 않는다. 이를 멋진 말로 표현하면 "타입 안정적" 이다~ 라고 표현 가능하다.

 

1.2 Underlying Type으로 초기화 하기 in C++17 & C++20 Using Enum 도입

C++11의 Enum Class는 우리가 명칭만 정할 수 있었지만, C++17부터는 Underlying Type을 도입할 수 있다.

 

underlying 을 해석하면 "밑에 있는" 이라는 뜻으로 enum class내부에 저장되는 타입이라고 보여진다.

 

결국 underlying type은 enumerators가 프로그램 안에서 실제로 저장되는 정수 형식 을 말한다.

 

해당 코드는 Gemini에게 C++17에서 어떻게 효율적으로 enum class를 그래서 쓸 수있는지 물어본 결과이다.

 

#include <iostream>
#include <cstdint>      // 1. 고정 크기 정수형(uint8_t 등)을 사용하기 위해 추가
#include <type_traits>  // 2. std::underlying_type_t를 사용하기 위해 추가

using namespace std;

// [변경점 1] Underlying Type 지정
// 기존에는 4바이트(int)를 차지했지만, : uint8_t를 붙여 정확히 1바이트만 쓰도록 메모리를 최적화했습니다.
enum class Color : uint8_t { 
    Red,
    Green,
    Blue 
};

enum class Color2 : uint8_t { 
    Red,
    Black,
    White 
};

enum class People : uint8_t { 
    Good = 1, // 원한다면 시작 값이나 특정 값을 명시적으로 줄 수도 있습니다.
    Bad = 0 
};

int main()
{
    int Green = 10;

    // [변경점 2] C++17 중괄호 직접 초기화 (Direct-list-initialization)
    // 센서나 네트워크에서 1바이트 정수 '1'(Green)을 받았다고 가정해 봅시다.
    uint8_t incomingData = 1; 
    
    // C++17부터는 static_cast 없이, Underlying Type이 일치하면 중괄호로 바로 안전하게 넣을 수 있습니다.
    Color x { incomingData }; 

    // 비교 연산은 기존 코드처럼 완벽하게 타입 안전(Type-safe)하게 동작합니다.
    if (x == Color::Red)
        cout << "It's Red\n";
    else
        cout << "It's not Red\n";

    People p = People::Good;

    if (p == People::Bad)
        cout << "Bad people\n";
    else
        cout << "Good people\n";


    // [변경점 3] 안전한 정수 값 추출
    // 기존 코드: cout << int(x); (위험한 C스타일 강제 캐스팅)
    
    // C++17 방식: 컴파일러가 Color의 원본 타입(uint8_t)을 스스로 알아내서 안전하게 벗겨냅니다.
    auto safeValue = static_cast<std::underlying_type_t<Color>>(x);
    
    // uint8_t는 콘솔에서 문자로(char) 깨져서 출력될 수 있으므로, 출력할 때만 잠시 int로 보여줍니다.
    cout << "x의 실제 정수값: " << static_cast<int>(safeValue) << "\n";

    return 0;
}

 

여기서 static_cast라는것이 나오는데 이것은 C++의 캐스팅 연산자로 다른 글에서 정리하도록 하겠다.

(아직 공부 안함)

 

이어서 바로 Using Enum 에 대해서 알아보자.

 

C++20에서 Using Enum이라는 키워드를 사용하면 Java에서 static을 선언하듯이 코드 양을 줄일 수 있다.

 

1.3 std::to_underlying in C++23

 

C++23 버전

enum class Permissions : uint8_t {
    Execute = 1,
	Write = 2,
    Read = 4
};

uint8_t value = static_cast<uint8_t>(Permissions::Read);

 

이전에는 명시적 캐스팅을 통해서면 변환이 가능했다.

 

C++23 이후 버전

#include <type_traits>

int main() {
    Permissions p = Permissions::Read;
    auto value = std::to_underlying(p); // C++23
}

 

2. union 과 struct

너무 이것들이 무엇인지 파악만 하는것은 재미없으니 Gemini에게 우선 깔쌈한 예시를 달라고 했다.

 

#include <iostream>
#include <cstdint>
#include <cstring>

// 메모리 패딩 방지: 구조체 크기를 변수 크기의 합과 정확히 일치시킴 (네트워크 통신 시 필수)
#pragma pack(push, 1)

// 로봇 제어 명령 패킷 (총 12바이트 고정)
struct RobotCommandPacket {
    uint8_t magicNumber; // 1바이트: 패킷 시작 식별자 (예: 0xAA)
    uint8_t commandType; // 1바이트: 0x01(이동명령), 0x02(센서설정)
    
    // 핵심: 바이트 배열과 개별 명령어 구조체들이 동일한 8바이트 메모리를 공유함
    union {
        // 네트워크 소켓에서 데이터를 통째로 밀어넣을 때 사용하는 버퍼
        uint8_t rawPayload[8]; 

        // commandType == 0x01 일 때 해석하는 뷰(View)
        struct {
            float targetVelocity; // 4바이트
            float steeringAngle;  // 4바이트
        } moveCommand;
        
        // commandType == 0x02 일 때 해석하는 뷰(View)
        struct {
            uint32_t sensorId;    // 4바이트
            uint8_t  targetMode;  // 1바이트
            uint8_t  reserved[3]; // 3바이트: 8바이트 크기를 맞추기 위한 더미 공간
        } configCommand;
    }; // 익명 유니온이므로 변수명 없이 내부 구조체에 바로 접근 가능
    
    uint16_t checksum; // 2바이트: 데이터 무결성 검증
};
#pragma pack(pop) // 패딩 방지 해제

void processNetworkStream(const uint8_t* recvBuffer) {
    // 1. 메모리 캐스팅 (Zero-Copy의 핵심)
    // 수신된 순수 바이트 배열(recvBuffer)의 시작 주소를 구조체 포인터로 덮어씌움
    const RobotCommandPacket* packet = reinterpret_cast<const RobotCommandPacket*>(recvBuffer);

    // 2. 파싱 과정 없이 즉시 데이터 사용
    if (packet->magicNumber != 0xAA) return;

    if (packet->commandType == 0x01) {
        // 이미 메모리 위치가 매핑되어 있으므로, 바이트를 float로 변환하는 연산이 필요 없음
        std::cout << "Velocity: " << packet->moveCommand.targetVelocity 
                  << ", Steering: " << packet->moveCommand.steeringAngle << "\n";
    } 
    else if (packet->commandType == 0x02) {
        std::cout << "Sensor ID: " << packet->configCommand.sensorId << "\n";
    }
}

 

코드를 자세히 보면 struct안에  union이 있고 또 union안에 struct가 있다. 

 

이게 뭐고 왜 이렇게 쓴건가?

 

2.1 Union

Union은 모든 멤버변수가 동일한 메모리 위치를 공유하는 사용자 정의 형식

 

다음 코드를 다시 들여다보자.

union {
    // 네트워크 소켓에서 데이터를 통째로 밀어넣을 때 사용하는 버퍼
    uint8_t rawPayload[8]; 

    // commandType == 0x01 일 때 해석하는 뷰(View)
    struct {
        float targetVelocity; // 4바이트
        float steeringAngle;  // 4바이트
    } moveCommand;

    // commandType == 0x02 일 때 해석하는 뷰(View)
    struct {
        uint32_t sensorId;    // 4바이트
        uint8_t  targetMode;  // 1바이트
        uint8_t  reserved[3]; // 3바이트: 8바이트 크기를 맞추기 위한 더미 공간
    } configCommand;
}; // 익명 유니온이므로 변수명 없이 내부 구조체에 바로 접근 가능

 

union에 대한 정의를 찾아보니 microsoft에서도 우리의 맨 위의 결과에 대해 말한다. variant class가 union에 대한 대안이라는데...? 하여간

 

https://learn.microsoft.com/ko-kr/cpp/cpp/unions?view=msvc-170

 

 

동일한 메모리 위치를 공유한다... 이게 도대체 무슨말인가?

 

그러니까 동일한 메모리를 공유한단다는건 메모리 공간이 하나라는 말이다.

 

원룸이라고 보면 된다. 원룸!

 

Struct는 구조체로 각자의 각자의 메모리 공간을 준다. 

 

MyStruct.cpp 예시

struct MyStruct {
    int i;    // 4바이트 방
    float f;  // 4바이트 방
    char c;   // 1바이트 방 (+ 패딩 3바이트)
}; // 총 크기: 12바이트

 

MyUnion.cpp 예시

union MyUnion {
    int i;    // 4바이트
    float f;  // 4바이트
    char c;   // 1바이트
}; // 총 크기: 가장 큰 멤버의 크기인 4바이트

 

 

그러면 이 코드를 정말 다시 들여다보자.

 

union {
    // 네트워크 소켓에서 데이터를 통째로 밀어넣을 때 사용하는 버퍼
    uint8_t rawPayload[8]; 

    // commandType == 0x01 일 때 해석하는 뷰(View)
    struct {
        float targetVelocity; // 4바이트
        float steeringAngle;  // 4바이트
    } moveCommand;

    // commandType == 0x02 일 때 해석하는 뷰(View)
    struct {
        uint32_t sensorId;    // 4바이트
        uint8_t  targetMode;  // 1바이트
        uint8_t  reserved[3]; // 3바이트: 8바이트 크기를 맞추기 위한 더미 공간
    } configCommand;
}; // 익명 유니온이므로 변수명 없이 내부 구조체에 바로 접근 가능

 

물리적 메모리 Byte 0 ~ 3 (4바이트) Byte 4 (1바이트) Byte 5 ~ 7 (3바이트)
뷰 1 (배열 안경) rawPayload[0-3] rawPayload[4] rawPayload[5~7]
뷰 2 (이동 안경) moveCommand.targetVelocity moveCommand.steeringAngle
(4바이트)
 
뷰 3 (설정 안경) configCommand.sensorId configCommand.targetMode configCommand.reserved[0~2]

 

말 그대로 union은 하나의 메모리를 공유하게 하는데, 이것을 또 내부에서 struct로 구분해서 사용할 수 있게 할 수 있다.

 

중간에 또 모르는 것이 있는데 #pragma pack(push,1) 과 #pragma pack(pop) 이라는 코드 사이에 union을 선언했다.

 

찾아보니 메모리 활용을 극한으로 끌어올리기 위해서 다음과 같이 선언하면 사이에 padding없이 사용할 메모리가 차례대로 들어간다고 한다. 

 

3. variant 사용법

많은 경우 union보다 variant가 사용하는 편이 더 간단하고 안전하다.

 

라는 말을 이해하기 위해서 이 글을 정리중이다. 

 

우선 비교군으로 enum class + union 사용 코드를 보겠다.

 

#include <iostream>
#include <string>

// 1. 현재 union이 무슨 타입인지 기억할 태그
enum class DataType {
    INT,
    FLOAT
    // 주의: 기존 union에는 std::string 같은 복잡한 C++ 객체를 
    // 그냥 넣으면 메모리 누수(Leak)가 발생하여 넣기 매우 까다롭습니다.
};

// 2. 태그와 유니온을 결합한 구조체
struct SensorData {
    DataType type;
    union {
        int intValue;
        float floatValue;
    }; 
};

void processData(const SensorData& data) {
    // 개발자가 직접 switch문으로 분기 처리를 해야 함
    switch (data.type) {
        case DataType::INT:
            std::cout << "정수 데이터: " << data.intValue << "\n";
            break;
        case DataType::FLOAT:
            std::cout << "실수 데이터: " << data.floatValue << "\n";
            break;
    }
}

int main() {
    SensorData myData;
    
    // 데이터를 넣을 때마다 type(태그)도 수동으로 꼭 바꿔줘야 함
    myData.type = DataType::FLOAT;
    myData.floatValue = 3.14f;

    // [치명적 위험] 실수로 타입은 FLOAT인데 INT를 읽어버리면?
    // 에러 없이 쓰레기값이 출력됨 (조용한 데이터 오염)
    // std::cout << myData.intValue; 

    processData(myData);
    return 0;
}

 

이 코드가 어떻게 변하는지 다음 코드를 보자.

 

#include <iostream>
#include <variant> // variant 사용을 위해 추가
#include <string>

// int, float, std::string 중 "하나만" 가질 수 있는 안전한 타입
using ModernSensorData = std::variant<int, float, std::string>;

void processData(const ModernSensorData& data) {
    // std::visit을 사용하면 컴파일러가 알아서 현재 타입에 맞는 코드를 실행해 줍니다.
    std::visit([](auto&& arg) {
        using T = std::decay_t<decltype(arg)>; // 현재 들어있는 데이터의 진짜 타입
        
        if constexpr (std::is_same_v<T, int>) {
            std::cout << "정수 데이터: " << arg << "\n";
        } 
        else if constexpr (std::is_same_v<T, float>) {
            std::cout << "실수 데이터: " << arg << "\n";
        } 
        else if constexpr (std::is_same_v<T, std::string>) {
            std::cout << "문자열 데이터: " << arg << "\n";
        }
    }, data);
}

int main() {
    ModernSensorData myData;

    // 1. 값만 대입하면 컴파일러가 알아서 내부 태그를 변경함 (수동 태그 관리 불필요)
    myData = 3.14f; // 현재 float 모드로 자동 전환됨
    processData(myData);

    myData = std::string("센서 정상 작동 중"); // string 모드로 자동 전환됨
    processData(myData);

    // 2. 완벽한 안전성 (Type Safety)
    myData = 42; // 현재 int 모드
    
    try {
        // int가 들어있는데 실수로 float를 꺼내려고 시도하면?
        float wrong = std::get<float>(myData); 
    } catch (const std::bad_variant_access& e) {
        // 기존 union처럼 쓰레기값을 조용히 반환하지 않고, 
        // 런타임에 즉시 예외를 던져 프로그램 오작동을 강력하게 막아줍니다.
        std::cout << "에러 방어 성공: 잘못된 타입에 접근했습니다!\n";
    }

    // 3. myData가 소멸될 때, 만약 내부에 std::string이 들어있었다면 
    // variant가 알아서 string의 소멸자를 호출하여 메모리를 깔끔하게 정리해 줍니다.

    return 0;
}

 

수많은 C++ 문법이 쏟아진다. 

 

필자는 현재 알아가는 단계이니 요약본으로 하나씩 정리하고 넘어가려고 한다.

 

3.1) 기본 데이터 타입과 입출력

std::string

C++의 표준 문자열 클래스. heap메모리의 동적 할당과 해제를 스스로 관리하는 RAII패턴을 따름. 

 

std::cout

C++의 표준 출력 스트림(Console Output). Java의  System.out.println과 같음.

 

3.2) 모던 유니온(Variant) 생태계

std::variant

C++17에 도입된 안전한 공용체(Type-safe Union).

자신이 현재 어떤 타입인지 내부적으로 태그(Index)를 기억 및 수명주기를 자동으로 관리해줌.

 

std::get

std::variant에서 값을 명시적으로 끄집어내는 함수.

 

std::bad_variant_access

std::get으로 잘못된 타입을 꺼내려 할 때 터지는 예외(Exception) 클래스

Java의 ClassCastException 와 비슷

 

std::visit

std::variant에 들어있는 값에 다형성 함수(보통 람다식)를 적용하는 방문자(Visitor) 패턴 유틸리티.

switch문을 대체한다.

 

3.3) 컴파일 타임 최적화 및 타입 추론 (메타프로그래밍)

delctype

"이 변수(또는 표현식)의 진짜 타입이 뭐야?"라고 컴파일러에게 물어보는 C++ 키워드.

 

std::decay_t

타입에 붙어있는 거추장스러운 수식어들을 싹 벗겨내고 순수한 원본 타입(Raw Type)만 남겨주는 변환기

 

constexpr (Constant Expression)

"이 코드는 프로그램 실행 전(컴파일 타임)에 미리 계산해서 상수로 굳혀라"라는 강력한 지시어

 

is_same_v

두 개의 타입이 완전히 똑같은지 비교하여 참/거짓(true/false)을 반환

 

여기서 variant 가 뭔지 나왔다.

C++17에 도입된 안전한 공용체(Type-safe Union).

 

union 과  enum class를 사용시에 일일히 타입이 맞는지 아닌지 검증하는 과정이 variant 를 사용해서 나름 간편하게 할 수있으니 좋다고 하는데, 또 글을 찾아보니 속도가 느리다는 불평이 생각보다 많다. 

 

이에 대한 글을 Gemini에게 물어서 해당 질문에 대한 답변을 첨부한다.

 

올려주신 캡처 이미지의 검색 결과들은 C++ 커뮤니티에서 아주 오랫동안, 그리고 치열하게 논쟁이 되어 온 '다형성(Polymorphism) 구현 방식에 따른 성능 비용'에 대한 핵심을 정확히 찌르고 있습니다.

특히 맨 위의 "성능 아니면 정신건강 둘 중 하나 골라야 됨"이라는 Reddit 글의 제목은 `union`(성능)과 `std::variant`(정신건강/안전성) 사이에서 고민하는 C++ 시스템 프로그래머들의 현실을 완벽하게 요약한 밈(Meme)이자 명언입니다.

이미지 내용에 언급된 "std::visit이 함수 포인터 테이블을 사용하고..."라는 문장을 중심으로, 왜 느린지 그 하드웨어적 원리를 해부하고 실무적인 타협점을 정리해 드리겠습니다.

---

### 1. `std::variant`와 `std::visit`은 왜 느린가? (병목의 원리)

가장 큰 병목은 값을 담을 때가 아니라, **`std::visit`을 통해 값을 꺼내어 로직을 실행할 때** 발생합니다.

**① 디스패치 테이블(Dispatch Table)과 간접 점프(Indirect Branch)**
`std::visit`에 람다 함수를 넘기면, 컴파일러는 `variant`가 가질 수 있는 모든 타입에 대해 함수 포인터들을 배열(테이블) 형태로 미리 만들어 둡니다.
프로그램이 실행될 때, CPU는 `variant` 내부의 '태그(Index)' 값을 읽고 $\rightarrow$ 테이블에서 몇 번째 함수로 갈지 찾은 다음 $\rightarrow$ 그 메모리 주소로 점프(Jump)하여 코드를 실행합니다.
이러한 간접적인 점프는 **CPU의 분기 예측(Branch Prediction)을 실패하게 만들 확률이 높아**, 파이프라인(명령어 처리 컨베이어 벨트)을 멈칫하게 만드는 원인이 됩니다.

**② 명령어 캐시 오염 (Instruction Cache Bloat)**
`variant`가 5개의 타입을 가질 수 있다면, `std::visit` 안에 적어둔 코드는 컴파일 시점에 5개의 서로 다른 기계어 뭉치로 복사 및 컴파일됩니다.
만약 서로 다른 `variant` 2개를 동시에 `visit` 한다면 ($5 \times 5 = 25$), 순식간에 25개의 함수 조합이 생성됩니다. 생성된 기계어 코드의 양이 기하급수적으로 팽창하면, CPU의 매우 작고 소중한 'L1 명령어 캐시(I-Cache)'에서 유용한 다른 코드들을 밀어내어 전체적인 시스템 지연을 유발합니다.

---

### 2. 객체지향 상속(Inheritance/Virtual)과의 객관적 비교

세 번째 검색 결과인 Stack Overflow의 질문(`std::variant vs. inheritance`)은 아키텍처 설계 시 반드시 짚고 넘어가야 할 부분입니다.

"느리다"는 것은 어디까지나 **아무런 안전장치가 없는 순수 `union` + `if/else`와 비교했을 때 느리다는 뜻**입니다. Java 백엔드에서 다루시던 인터페이스(Interface) 기반의 동적 다형성, 즉 C++의 **가상 함수(`virtual` function) 기반 상속보다는 `std::variant`가 성능상 훨씬 유리한 경우가 많습니다.**

* **가상 함수(상속):** 객체가 힙(Heap) 메모리 여기저기에 흩어져 동적으로 할당됩니다. 함수를 호출할 때마다 포인터를 타고 멀리 있는 메모리를 조회해야 하므로 데이터 캐시 미스(D-Cache Miss)가 빈번하게 발생합니다.
* **`std::variant`:** 모든 데이터가 스택(Stack) 또는 배열 내에 연속적인 메모리 블록으로 뭉쳐 있습니다. CPU가 데이터를 읽어오는 속도 자체가 상속 구조보다 압도적으로 빠릅니다.

따라서 힙 할당과 가상 함수 오버헤드를 피하기 위한 목적으로 `std::variant`를 선택하는 것은 매우 훌륭한 최적화 전략입니다.

---

### 3. 실무 최적화 가이드: 어떻게 사용해야 하는가?

로보틱스와 임베디드 제어 시스템의 계층(Layer)에 따라 사용 전략을 분리해야 합니다.

**전략 A. 초당 수천 번 도는 핵심 제어 루프 (Critical Path)**
자율주행의 조향/가속 PID 제어 루프처럼 1ms의 지연도 허용되지 않는 구간에서는 `std::visit`의 사용을 피하거나 다른 방식을 택해야 합니다.

1. **`std::get_if`를 활용한 바이패스:**
만약 현재 루프에서 특정 한 가지 타입(예: `SensorData` 중에서도 `FloatData` 타입)만 집중적으로 처리하면 되는 상황이라면, 무거운 `std::visit` 대신 예외를 던지지 않고 포인터만 가볍게 반환하는 `std::get_if`를 사용하여 디스패치 테이블을 우회합니다.
```cpp
// 함수 포인터 테이블을 거치지 않고, 빠르고 직접적으로 메모리 접근
if (const float* value = std::get_if<float>(&myVariant)) {
    // 제어 로직 수행
}

```


2. **원시적인 Tagged Union 회귀:**
1바이트의 메모리 패딩조차 병목이 되는 극단적인 하드웨어 I/O 구간에서는 앞서 배운 `enum class` + 익명 `union` 패턴으로 회귀하는 것이 맞습니다. (Reddit 글의 '성능'을 택하는 구역)

**전략 B. 시스템 아키텍처 및 메시징 계층 (High-level Logic)**
ROS2 노드 간의 상태 전달, 로봇의 전체 상태 머신(State Machine) 전환, 로그 서버와의 통신 같은 상위 로직에서는 **무조건 `std::variant`와 `std::visit`을 기본으로 사용해야 합니다.**

1. **타입의 개수($N$)를 작게 유지:** `variant` 안에 들어갈 수 있는 타입의 종류를 3~5개 이내로 묶어 설계합니다. 타입이 적으면 컴파일러가 함수 포인터 테이블 대신 일반적인 `if-else`나 `switch`문으로 최적화(Devirtualization)를 아주 잘해줍니다.
2. **안전성(정신건강)의 경제적 가치:** 제어 루프가 아닌 곳에서 `visit`으로 인해 손해 보는 속도는 기껏해야 수십 나노초(ns)입니다. 반면 `union`을 잘못 다루어 발생하는 메모리 오염 버그를 디버깅하는 데는 며칠의 야근이 필요합니다.

**결론:**
검색 결과의 우려들은 C++ 특유의 '극한까지 쥐어짜는' 문화에서 비롯된 기형적인 아키텍처(variant 안에 20개 이상의 타입을 넣거나, 복수의 variant를 동시 visit 하는 등)를 경계하는 내용에 가깝습니다. 타입의 개수를 작게 유지하고 적재적소에 `std::get_if`를 섞어 쓴다면, 성능과 정신건강(안전성)을 모두 챙길 수 있습니다.

 

뭐... 그렇다고 한다...


필자는 Java개발자로 나의 능력의 스펙트럼을 넓히기 위해 C++를 현재 찾아보고 익숙해지기 위해 여러 글을 찾아보고 있다.

 

해당 글은 사실 미흡할지 모르지만 C++을 알아가는 하나의 과정으로 이해해주면 좋겠다.


 

참고:

https://www.cppstories.com/2024/enum-improvements/

https://www.geeksforgeeks.org/cpp/enum-classes-in-c-and-their-advantage-over-enum-datatype/

https://learn.microsoft.com/ko-kr/cpp/cpp/unions?view=msvc-170

https://www.reddit.com/r/cpp/comments/kst2pu/with_stdvariant_you_choose_either_performance_or/?tl=ko