본문으로 건너뛰기 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.

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("alice@example.com", "Hello");
sendEmail("bob@example.com", "Hi", "charlie@example.com");

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


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. std::optional vs nullptr: optional models absent values with type safety; pointers for non-owning observers, polymorphis…

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, optional, pointer, C++17, null safety, type safety.