[2026] C++ any | 타입 소거 가이드
이 글의 핵심
std::any와 variant·void* 비교, 타입 안전성, any_cast, 실전 사례와 성능 오버헤드를 정리한 가이드입니다.
any란?
std::any 는 C++17에서 도입된 타입 소거 컨테이너입니다. 어떤 타입의 값이든 저장할 수 있으며, 런타임에 타입을 확인하고 값을 추출할 수 있습니다. void*의 타입 안전한 대안입니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <any>
// 실행 예제
std::any a = 42; // int
a = 3.14; // double
a = std::string{"hello"}; // string
// 타입 확인 후 접근
if (a.type() == typeid(std::string)) {
std::cout << std::any_cast<std::string>(a) << std::endl;
}
왜 필요한가?:
- 타입 유연성: 컴파일 타임에 타입을 알 수 없을 때
- 타입 안전:
void*보다 안전한 타입 소거 - 자동 관리: 생명주기 자동 관리
- 예외 안전: 잘못된 타입 접근 시 예외 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ void*: 타입 불안전, 수동 관리
void* ptr = new int(42);
int x = *static_cast<int*>(ptr); // 타입 확인 없음
delete ptr; // 수동 삭제
// ✅ std::any: 타입 안전, 자동 관리
std::any a = 42;
if (a.type() == typeid(int)) {
int x = std::any_cast<int>(a); // 타입 확인
}
// 자동 소멸
any의 동작 원리:
std::any는 내부적으로 타입 정보와 값을 함께 저장합니다. 작은 객체는 스택에, 큰 객체는 힙에 저장합니다 (Small Object Optimization).
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 개념적 구현
// 타입 정의
class any {
void* data_;
const std::type_info* type_;
void (*deleter_)(void*);
public:
template<typename T>
any(T value) {
data_ = new T(std::move(value));
type_ = &typeid(T);
deleter_ = [](void* p) { delete static_cast<T*>(p); };
}
~any() {
if (data_) {
deleter_(data_);
}
}
const std::type_info& type() const {
return *type_;
}
};
any vs variant vs optional:
| 특징 | std::any | std::variant | std::optional |
|---|---|---|---|
| 저장 가능 타입 | 모든 타입 | 정해진 타입 | 단일 타입 |
| 타입 추적 | 런타임 | 컴파일 타임 | N/A |
| 메모리 | 힙 (큰 객체) | 스택 | 스택 |
| 성능 | 느림 | 빠름 | 매우 빠름 |
| 타입 안전 | 런타임 | 컴파일 타임 | 컴파일 타임 |
| 용도 | 플러그인, 설정 | 상태 머신, 에러 | null 대안 |
| 아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다. |
// any: 모든 타입 가능
std::any a = 42;
a = std::string{"hello"};
a = std::vector<int>{1, 2, 3};
// variant: 정해진 타입만
std::variant<int, std::string> v = 42;
v = std::string{"hello"};
// v = std::vector<int>{}; // 에러
// optional: 단일 타입
std::optional<int> opt = 42;
opt = std::nullopt;
any vs variant vs void* 심화
void*
- 장점: 어떤 주소든 담을 수 있고, C API와의 연동이 단순해 보입니다.
- 단점: 소유권·수명·실제 타입이 코드 계약에만 의존합니다. 잘못된
static_cast는 정의되지 않은 동작이고,delete와 짝이 맞지 않으면 즉시 UB입니다. - 요약: 레이아웃이 고정된 불투명 버퍼(예: FFI 버퍼)에는 여전히 쓰이지만, 소유 객체를 담는 용도로는
std::any나 스마트 포인터+인터페이스가 안전합니다. std::variant - 닫힌 집합(예:
int | double | string)이 컴파일 타임에 고정되어 있으면visit·holds_alternative로 대부분의 실수를 컴파일 단계에서 잡을 수 있습니다. - 메모리는 보통 고정 크기 버퍼에 두어 힙 할당 없이 동작하는 경우가 많습니다(구현 의존). std::any
- 저장 타입 집합이 열리고, 플러그인처럼 미리 알 수 없는 타입이 들어올 수 있을 때 적합합니다.
- 그 대가로 타입 검사는 런타임에 가며, 잘못된
any_cast는 예외(bad_any_cast)입니다. 선택 가이드 한 줄: 타입 후보가 정해져 있으면 variant, 힙 포인터만 넘기면 되고 규약이 명확하면void*(또는uintptr_t), 그 사이에서 “값 소유 + 임의 타입”이면 any.
타입 안전성
std::any의 안전성은 “잘못된 캐스트를 막아 주는가?”가 아니라 “잘못된 캐스트를 런타임에 감지할 수 있는가?”에 가깝습니다.
any_cast<T>(a)는 내부 저장 타입이T와 정확히 일치해야 합니다(참조·cv 한정도 규칙이 있음).int를 넣고long으로 꺼내려 하면 실패합니다.type()은type_info를 돌려주므로if (a.type() == typeid(Foo))패턴이 가능하지만, 유지보수는any_cast한 번에 맡기는 편이 낫습니다.- 인터페이스 경계에서는 가능하면
variant나 전용 베이스 클래스로 타입을 제한하고,any는 정말로 이기종이 필요한 층에만 두는 것이 안전합니다.
any_cast 완전 정리
값으로 복사
std::any a = std::string{"hi"};
std::string s = std::any_cast<std::string>(a); // 복사
참조로 수정
std::any a = 10;
std::any_cast<int&>(a) = 20;
포인터 오버로드(실패 시 nullptr) 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 실행 예제
std::any a = 3.14;
if (double* p = std::any_cast<double>(&a)) {
*p = 2.71;
}
any_cast는 저장된 타입과 요청 타입이 맞지 않으면 값/참조 오버로드에서 bad_any_cast를 던집니다. 포인터 오버로드는 예외 대신 nullptr을 돌려주므로, 핫 경로에서는 포인터 형태로 분기하는 선택이 있습니다.
이동-only 타입
복사 불가·이동만 가능한 타입은 std::make_any<T>(...)로 넣고, 꺼낼 때 std::any_cast<T&>(a) 또는 any_cast<T>(std::move(a)) 패턴을 타입에 맞게 사용합니다.
성능 오버헤드
대략적인 비용 요소는 다음과 같습니다.
- 타입 정보:
type_info조회,any_cast시 내부 비교. - 저장 방식: 구현에 따라 Small Object Optimization으로 작은 객체는 인라인 버퍼에 두지만, 큰 객체나 복잡한 타입은 힙 할당이 붙을 수 있습니다.
- 복사/이동:
any자체를 자주 복사하면 저장된 값의 복사 비용까지 함께 갑니다. 완화
- 후보 타입이 정해져 있으면 std::variant 로 옮깁니다.
- 같은 스코프에서 반복 캐스트한다면 한 번만 캐스트해 참조를 잡거나, 애초에
variant/구체 타입으로 받습니다. - 설정 맵 등에서는
string키 +any값보다 강한 타입의 struct나toml/json파서 결과 타입이 더 낫지 않은지 검토합니다.
실전 활용 요약
| 상황 | any가 적합한 이유 |
|---|---|
| 스크립트·플러그인이 임의 타입 페이로드를 넘김 | 타입을 미리 열거하기 어려움 |
| 설정 파일 값이 int/string/bool 등 혼재 | 단순 키-값 저장소로 편리(단, 스키마가 커지면 전용 타입 고려) |
| 이벤트 버스에 다양한 페이로드 | 핸들러에서 any_cast로 분기 |
| 테스트 목 객체·모의 의존성 | 제한된 범위에서만 사용 |
반대로 수치 핫 루프, 실시간 오디오 샘플 처리, 내부 API처서 타입이 고정이면 any는 피하고 variant나 직접 타입을 쓰는 편이 낫습니다. |
기본 사용
아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <any>
// 생성
std::any a1 = 42;
std::any a2 = std::string{"hello"};
std::any a3; // 빈 any
// 확인
if (a1.has_value()) {
std::cout << "값 있음" << std::endl;
}
// 타입
std::cout << a1.type().name() << std::endl;
실전 예시
예시 1: 이기종 컨테이너
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <any>
#include <vector>
#include <string>
int main() {
std::vector<std::any> data;
data.push_back(42);
data.push_back(3.14);
data.push_back(std::string{"hello"});
for (const auto& item : data) {
if (item.type() == typeid(int)) {
std::cout << "int: " << std::any_cast<int>(item) << std::endl;
} else if (item.type() == typeid(double)) {
std::cout << "double: " << std::any_cast<double>(item) << std::endl;
} else if (item.type() == typeid(std::string)) {
std::cout << "string: " << std::any_cast<std::string>(item) << std::endl;
}
}
}
예시 2: 설정 저장소
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <any>
#include <map>
#include <string>
class Config {
std::map<std::string, std::any> settings;
public:
template<typename T>
void set(const std::string& key, const T& value) {
settings[key] = value;
}
template<typename T>
T get(const std::string& key) const {
auto it = settings.find(key);
if (it != settings.end()) {
return std::any_cast<T>(it->second);
}
throw std::runtime_error("키 없음");
}
};
int main() {
Config config;
config.set("port", 8080);
config.set("host", std::string{"localhost"});
config.set("timeout", 30.0);
int port = config.get<int>("port");
std::string host = config.get<std::string>("host");
double timeout = config.get<double>("timeout");
}
예시 3: 이벤트 시스템
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <any>
#include <functional>
#include <map>
#include <string>
class EventBus {
std::map<std::string, std::vector<std::function<void(std::any)>>> handlers;
public:
void on(const std::string& event, std::function<void(std::any)> handler) {
handlers[event].push_back(handler);
}
void emit(const std::string& event, std::any data) {
if (auto it = handlers.find(event); it != handlers.end()) {
for (auto& handler : it->second) {
handler(data);
}
}
}
};
int main() {
EventBus bus;
bus.on("message", [](std::any data) {
auto msg = std::any_cast<std::string>(data);
std::cout << "메시지: " << msg << std::endl;
});
bus.on("count", [](std::any data) {
auto count = std::any_cast<int>(data);
std::cout << "카운트: " << count << std::endl;
});
bus.emit("message", std::string{"Hello"});
bus.emit("count", 42);
}
예시 4: 타입 안전 래퍼
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <any>
class SafeAny {
std::any data;
public:
template<typename T>
void set(const T& value) {
data = value;
}
template<typename T>
std::optional<T> get() const {
try {
return std::any_cast<T>(data);
} catch (const std::bad_any_cast&) {
return std::nullopt;
}
}
bool empty() const {
return !data.has_value();
}
};
int main() {
SafeAny sa;
sa.set(42);
if (auto val = sa.get<int>()) {
std::cout << "값: " << *val << std::endl;
}
if (auto val = sa.get<double>()) {
std::cout << "double" << std::endl;
} else {
std::cout << "타입 불일치" << std::endl;
}
}
값 접근
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::any a = 42;
// any_cast: 값
int x = std::any_cast<int>(a);
// any_cast: 포인터
if (int* ptr = std::any_cast<int>(&a)) {
std::cout << *ptr << std::endl;
}
// any_cast: 참조
int& ref = std::any_cast<int&>(a);
자주 발생하는 문제
문제 1: 타입 불일치
아래 코드는 cpp를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::any a = 42;
// ❌ 잘못된 타입
try {
double d = std::any_cast<double>(a); // std::bad_any_cast
} catch (const std::bad_any_cast&) {
std::cout << "타입 불일치" << std::endl;
}
// ✅ 확인 후 접근
if (a.type() == typeid(int)) {
int x = std::any_cast<int>(a);
}
문제 2: 참조
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::any a = 42;
// ❌ 복사
int x = std::any_cast<int>(a);
// ✅ 참조
int& ref = std::any_cast<int&>(a);
ref = 100;
std::cout << std::any_cast<int>(a) << std::endl; // 100
문제 3: 성능
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// any는 오버헤드 있음
// - 타입 정보 저장
// - 동적 할당 (큰 객체)
// - 타입 체크
// ✅ 대안: variant (타입 알려진 경우)
std::variant<int, double, std::string> v;
문제 4: 복사
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
struct NonCopyable {
NonCopyable() = default;
NonCopyable(const NonCopyable&) = delete;
};
// ❌ 복사 불가 타입
// std::any a = NonCopyable{}; // 에러
// ✅ 이동
std::any a = std::make_any<NonCopyable>();
any vs variant
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// any: 모든 타입 (런타임)
std::any a = 42;
a = std::string{"hello"};
// variant: 정해진 타입 (컴파일 타임)
std::variant<int, std::string> v = 42;
v = std::string{"hello"};
// variant 권장 (타입 알려진 경우)
실무 패턴
패턴 1: 플러그인 시스템
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class Plugin {
public:
virtual ~Plugin() = default;
virtual std::string getName() const = 0;
virtual std::any execute(const std::any& input) = 0;
};
class PluginManager {
std::map<std::string, std::unique_ptr<Plugin>> plugins_;
public:
void registerPlugin(std::unique_ptr<Plugin> plugin) {
plugins_[plugin->getName()] = std::move(plugin);
}
std::any execute(const std::string& name, const std::any& input) {
if (auto it = plugins_.find(name); it != plugins_.end()) {
return it->second->execute(input);
}
throw std::runtime_error("플러그인 없음");
}
};
// 플러그인 구현
class CalculatorPlugin : public Plugin {
public:
std::string getName() const override {
return "calculator";
}
std::any execute(const std::any& input) override {
auto values = std::any_cast<std::vector<int>>(input);
int sum = 0;
for (int v : values) {
sum += v;
}
return sum;
}
};
패턴 2: 타입 안전 메시지 버스
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 반복문으로 데이터를 처리합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class MessageBus {
struct Handler {
std::function<void(const std::any&)> callback;
std::type_index expectedType;
};
std::map<std::string, std::vector<Handler>> handlers_;
public:
template<typename T>
void subscribe(const std::string& topic, std::function<void(const T&)> callback) {
handlers_[topic].push_back({
[callback](const std::any& data) {
callback(std::any_cast<const T&>(data));
},
std::type_index(typeid(T))
});
}
template<typename T>
void publish(const std::string& topic, const T& data) {
if (auto it = handlers_.find(topic); it != handlers_.end()) {
for (auto& handler : it->second) {
if (handler.expectedType == std::type_index(typeid(T))) {
try {
handler.callback(std::any(data));
} catch (const std::bad_any_cast& e) {
std::cerr << "타입 불일치: " << e.what() << '\n';
}
}
}
}
}
};
// 사용
MessageBus bus;
bus.subscribe<std::string>("log", [](const std::string& msg) {
std::cout << "로그: " << msg << '\n';
});
bus.publish("log", std::string{"Hello"});
패턴 3: 동적 속성 시스템
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class Entity {
std::map<std::string, std::any> properties_;
public:
template<typename T>
void setProperty(const std::string& name, const T& value) {
properties_[name] = value;
}
template<typename T>
std::optional<T> getProperty(const std::string& name) const {
auto it = properties_.find(name);
if (it == properties_.end()) {
return std::nullopt;
}
try {
return std::any_cast<T>(it->second);
} catch (const std::bad_any_cast&) {
return std::nullopt;
}
}
bool hasProperty(const std::string& name) const {
return properties_.count(name) > 0;
}
};
// 사용
Entity player;
player.setProperty("health", 100);
player.setProperty("name", std::string{"Hero"});
player.setProperty("position", std::vector<double>{10.0, 20.0});
if (auto health = player.getProperty<int>("health")) {
std::cout << "체력: " << *health << '\n';
}
FAQ
Q1: any는 무엇인가요?
A: C++17의 타입 소거 컨테이너로, 어떤 타입의 값이든 저장할 수 있습니다. 런타임에 타입을 확인하고 값을 추출합니다.
std::any a = 42;
a = 3.14;
a = std::string{"hello"};
Q2: any는 어디에 사용하나요?
A:
- 이기종 컨테이너: 다양한 타입을 하나의 컨테이너에 저장
- 플러그인 시스템: 플러그인 간 데이터 전달
- 설정 저장소: 다양한 타입의 설정 값 저장
- 이벤트 시스템: 다양한 타입의 이벤트 데이터 다음은 간단한 cpp 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
std::vector<std::any> data;
data.push_back(42);
data.push_back(3.14);
data.push_back(std::string{"hello"});
Q3: 값에 어떻게 접근하나요?
A: std::any_cast 를 사용합니다. 타입 불일치 시 예외를 던집니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::any a = 42;
// 값 추출 (예외 가능)
int x = std::any_cast<int>(a);
// 포인터 추출 (안전)
if (int* ptr = std::any_cast<int>(&a)) {
std::cout << *ptr << '\n';
}
// 참조 추출
int& ref = std::any_cast<int&>(a);
Q4: any의 성능은?
A: 오버헤드가 있습니다. 타입 정보 저장, 동적 할당 (큰 객체), 타입 체크 비용이 있습니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// any: 오버헤드
std::any a = 42; // 타입 정보 + 값 저장
// variant: 더 빠름 (타입 알려진 경우)
std::variant<int, double, std::string> v = 42;
권장: 타입을 미리 알 수 있으면 variant 사용
Q5: variant과 어떤 차이가 있나요?
A:
- any: 모든 타입 가능, 런타임 타입 체크, 느림
- variant: 정해진 타입만, 컴파일 타임 타입 체크, 빠름 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// any: 모든 타입
std::any a = 42;
a = std::vector<int>{1, 2, 3}; // OK
// variant: 정해진 타입만
std::variant<int, double> v = 42;
// v = std::vector<int>{}; // 에러
선택 기준:
- 타입을 미리 알 수 있으면: variant
- 타입을 미리 알 수 없으면: any
Q6: any는 참조를 저장할 수 있나요?
A: 직접은 불가능하지만, std::reference_wrapper를 사용할 수 있습니다.
아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
int x = 42;
// ❌ 참조 저장 불가
// std::any a{x}; // 복사됨
// ✅ reference_wrapper 사용
std::any a = std::ref(x);
std::reference_wrapper<int> ref = std::any_cast<std::reference_wrapper<int>>(a);
ref.get() = 100;
std::cout << x << '\n'; // 100
Q7: any의 메모리 할당은?
A: Small Object Optimization (SOO) 을 사용합니다. 작은 객체는 스택에, 큰 객체는 힙에 저장합니다. 아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 작은 객체: 스택 (보통 16-32바이트 이하)
std::any a1 = 42; // 스택
// 큰 객체: 힙
std::any a2 = std::vector<int>(1000); // 힙
Q8: any 학습 리소스는?
A:
- “C++17 The Complete Guide” by Nicolai Josuttis
- “Effective Modern C++” by Scott Meyers
- cppreference.com - std::any
관련 글: variant, optional, type_erasure.
한 줄 요약:
std::any는 어떤 타입이든 저장할 수 있는 C++17 타입 소거 컨테이너입니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.