본문으로 건너뛰기
Previous
Next
C++ std::optional vs Pointers: Representing No Value

C++ std::optional vs Pointers: Representing No Value

C++ std::optional vs Pointers: Representing No Value

이 글의 핵심

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](/en/blog/cpp-comparison-04-shared-unique-ptr/.

Introduction: “How should I represent null?”

C++ offers nullptr pointers and std::optional (C++17) for “maybe no value,” with very different safety and cost profiles.

This article covers:

  • optional vs pointers
  • Type safety
  • Performance
  • Scenarios

1. Comparison

Aspectstd::optionalT*
StorageUsually inline (stack)Address-sized (points elsewhere)
Ownership modelOwns value stateAliases external object
Absent valuestd::nulloptnullptr
SafetyExplicit checks requiredEasy to forget null check
Heap allocationNo (unless T itself allocates)Depends on what it points to
Sizesizeof(T) + 1 (approx)sizeof(void*)

2. Type safety

std::optional forces explicit handling

#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

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

#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)

Operationoptionalint* (stack)int* (heap)
Create2ms2ms450ms
Check + access3ms3ms3ms
Destroy0ms0ms420ms

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

#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

#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("[email protected]", "Hello");
sendEmail("[email protected]", "Hi", "[email protected]");

Scenario 3: Lazy initialization

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

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:

  1. Return value may be absent: Parsing, lookups, validation

    std::optional<int> parseInt(const std::string& s);
  2. Optional struct members: Avoid sentinel values

// 타입 정의
   struct Config {
       std::string host;
       std::optional<int> port;  // May not be specified
   };
  1. Avoiding heap allocation: Small types that don’t need indirection
    std::optional<int> cachedValue;  // Stack storage

Use pointers when:

  1. Polymorphism: Virtual dispatch

    std::unique_ptr<Base> obj;
  2. Large objects: Avoid copying

    void process(const LargeObject* obj);  // Non-owning
  3. Array/buffer access: Pointing into existing memory

    int* begin = array;
    int* end = array + size;
  4. Shared ownership: Multiple owners

    std::shared_ptr<Resource> shared;

Common mistakes

Mistake 1: Dereferencing without checking

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

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

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)

// 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

#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

Compilerstd::optionalMonadic operations
GCC7+12+ (C++23)
Clang4+16+ (C++23)
MSVC2017 15.3+2022 17.4+ (C++23)

Keywords

std::optional, optional vs pointer, C++17, null safety, type safety, value semantics, nullptr


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. std::optional vs nullptr: optional models absent values with type safety; pointers for non-owning observers, polymorphis… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • [C++ shared_ptr vs unique_ptr: Smart Pointer Choice Complete](/en/blog/cpp-comparison-04-shared-unique-ptr/
  • [C++ std::variant vs union Complete Comparison](/en/blog/cpp-comparison-13-variant-union/
  • [C++ std::any vs void* Complete Comparison](/en/blog/cpp-comparison-14-any-void-pointer/

이 글에서 다루는 키워드 (관련 검색어)

C++, optional, pointer, C++17, null safety, type safety 등으로 검색하시면 이 글이 도움이 됩니다.