[2026] C++ std::optional vs Pointers: Representing No Value Safely
이 글의 핵심
std::optional vs nullptr: optional models absent values with type safety; pointers for non-owning observers, polymorphism, and shared ownership. Stack-friendly optional vs pointer costs.
When you need owning heap storage instead of optional, compare shared_ptr vs unique_ptr.
Introduction: “How should I represent null?”
C++ offers nullptr pointers and std::optional
- optional vs pointers
- Type safety
- Performance
- Scenarios
1. Comparison
| Aspect | std::optional | T* |
|---|---|---|
| Storage | Usually inline (stack) | Address-sized (points elsewhere) |
| Ownership model | Owns value state | Aliases external object |
| Absent value | std::nullopt | nullptr |
| Safety | Explicit checks required | Easy to forget null check |
| Heap allocation | No (unless T itself allocates) | Depends on what it points to |
| Size | sizeof(T) + 1 (approx) | sizeof(void*) |
2. Type safety
std::optional forces explicit handling
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <optional>
#include <string>
std::optional<std::string> findUser(int id) {
if (id == 1) {
return "Alice";
}
return std::nullopt; // Explicit "no value"
}
// Usage
auto user = findUser(1);
if (user.has_value()) {
std::cout << *user << "\n"; // Safe access
}
// Or with value_or
std::string name = findUser(2).value_or("Guest");
Pointers allow silent null dereference
다음은 cpp를 활용한 상세한 구현 코드입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::string* findUserPtr(int id) {
static std::string alice = "Alice";
if (id == 1) {
return &alice;
}
return nullptr;
}
// ❌ Easy to forget null check
auto* user = findUserPtr(2);
std::cout << *user << "\n"; // Crash!
// ✅ Must remember to check
if (user != nullptr) {
std::cout << *user << "\n";
}
3. Performance
Memory layout
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <optional>
#include <iostream>
struct Small {
int value;
};
struct Large {
char data[1000];
};
int main() {
std::cout << "int: " << sizeof(int) << "\n"; // 4
std::cout << "optional<int>: " << sizeof(std::optional<int>) << "\n"; // 8
std::cout << "int*: " << sizeof(int*) << "\n"; // 8
std::cout << "Large: " << sizeof(Large) << "\n"; // 1000
std::cout << "optional<Large>: " << sizeof(std::optional<Large>) << "\n"; // ~1008
std::cout << "Large*: " << sizeof(Large*) << "\n"; // 8
}
Benchmark (GCC 13, -O3, 1M operations)
| Operation | optional | int* (stack) | int* (heap) |
|---|---|---|---|
| Create | 2ms | 2ms | 450ms |
| Check + access | 3ms | 3ms | 3ms |
| Destroy | 0ms | 0ms | 420ms |
Key insight: optional avoids heap allocation for small types. For large types, pointer indirection may be better. |
4. Real-world scenarios
Scenario 1: Optional return values
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <optional>
#include <string>
#include <map>
class UserDatabase {
std::map<int, std::string> users_;
public:
std::optional<std::string> findUser(int id) const {
auto it = users_.find(id);
if (it != users_.end()) {
return it->second;
}
return std::nullopt;
}
// Alternative: pointer version (less safe)
const std::string* findUserPtr(int id) const {
auto it = users_.find(id);
return (it != users_.end()) ? &it->second : nullptr;
}
};
// Usage comparison
UserDatabase db;
// optional: explicit handling
if (auto user = db.findUser(1)) {
std::cout << *user << "\n";
}
// pointer: easy to forget check
auto* user = db.findUserPtr(1);
if (user) { // Must remember!
std::cout << *user << "\n";
}
Scenario 2: Optional function parameters
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <optional>
#include <string>
void sendEmail(const std::string& to,
const std::string& subject,
std::optional<std::string> cc = std::nullopt) {
std::cout << "To: " << to << "\n";
std::cout << "Subject: " << subject << "\n";
if (cc) {
std::cout << "CC: " << *cc << "\n";
}
}
// Usage
sendEmail("alice@example.com", "Hello");
sendEmail("bob@example.com", "Hi", "charlie@example.com");
Scenario 3: Lazy initialization
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class ExpensiveResource {
mutable std::optional<std::string> cache_;
public:
const std::string& getData() const {
if (!cache_) {
cache_ = computeExpensiveData(); // Lazy init
}
return *cache_;
}
private:
std::string computeExpensiveData() const {
// Expensive computation
return "computed data";
}
};
Scenario 4: Polymorphism requires pointers
다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
class Base {
public:
virtual void process() = 0;
virtual ~Base() = default;
};
class Derived : public Base {
public:
void process() override { std::cout << "Derived\n"; }
};
// ❌ Cannot use optional for polymorphism
// std::optional<Base> obj; // Error: Base is abstract
// ✅ Must use pointer
std::unique_ptr<Base> obj = std::make_unique<Derived>();
obj->process();
When to use each
Use std::optional when:
- Return value may be absent: Parsing, lookups, validation
std::optional<int> parseInt(const std::string& s); - Optional struct members: Avoid sentinel values
아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 타입 정의
struct Config {
std::string host;
std::optional<int> port; // May not be specified
};
- Avoiding heap allocation: Small types that don’t need indirection
std::optional<int> cachedValue; // Stack storage
Use pointers when:
- Polymorphism: Virtual dispatch
std::unique_ptr<Base> obj; - Large objects: Avoid copying
void process(const LargeObject* obj); // Non-owning - Array/buffer access: Pointing into existing memory
int* begin = array; int* end = array + size; - Shared ownership: Multiple owners
std::shared_ptr<Resource> shared;
Common mistakes
Mistake 1: Dereferencing without checking
다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::optional<int> opt = std::nullopt;
// ❌ Undefined behavior
int x = *opt;
// ✅ Check first
if (opt) {
int x = *opt;
}
// ✅ Or use value_or
int x = opt.value_or(0);
// ✅ Or use value() with exception
try {
int x = opt.value(); // Throws std::bad_optional_access
} catch (const std::bad_optional_access&) {
// Handle
}
Mistake 2: Dangling pointer from optional
아래 코드는 cpp를 사용한 구현 예제입니다. 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
std::optional<std::string> getString() {
return "hello";
}
// ❌ Dangling pointer
const char* ptr = getString()->c_str(); // Temporary destroyed!
// ✅ Store optional first
auto opt = getString();
if (opt) {
const char* ptr = opt->c_str(); // Safe
}
Mistake 3: Using optional for large objects
아래 코드는 cpp를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 코드를 직접 실행해보면서 동작을 확인해보세요.
struct HugeData {
char buffer[10000];
};
// ❌ Wastes stack space
std::optional<HugeData> opt; // ~10KB on stack even when empty
// ✅ Better: use unique_ptr
std::unique_ptr<HugeData> ptr; // 8 bytes, heap when needed
Advanced patterns
Optional chaining (monadic operations C++23)
다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// C++23: transform, and_then, or_else
std::optional<int> opt = 42;
auto result = opt
.transform([](int x) { return x * 2; })
.and_then([](int x) -> std::optional<int> {
return x > 50 ? std::optional(x) : std::nullopt;
})
.or_else([] { return std::optional(0); });
// C++17 manual equivalent
std::optional<int> result;
if (opt) {
int doubled = *opt * 2;
if (doubled > 50) {
result = doubled;
}
}
if (!result) {
result = 0;
}
Optional reference wrapper
다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
#include <optional>
#include <functional>
// optional cannot hold references directly
// std::optional<int&> ref; // ❌ Error
// ✅ Use reference_wrapper
std::optional<std::reference_wrapper<int>> ref;
int x = 10;
ref = std::ref(x);
if (ref) {
ref->get() = 20; // Modifies x
}
Compiler support
| Compiler | std::optional | Monadic operations |
|---|---|---|
| GCC | 7+ | 12+ (C++23) |
| Clang | 4+ | 16+ (C++23) |
| MSVC | 2017 15.3+ | 2022 17.4+ (C++23) |
Related posts
Keywords
std::optional, optional vs pointer, C++17, null safety, type safety, value semantics, nullptr