Skip to content

Instantly share code, notes, and snippets.

@DrkWithT
Last active July 7, 2024 18:11
Show Gist options
  • Save DrkWithT/69b86d4a2895ba607848042c9aa55993 to your computer and use it in GitHub Desktop.
Save DrkWithT/69b86d4a2895ba607848042c9aa55993 to your computer and use it in GitHub Desktop.
A toy C++ optional implementation for educational purposes. Follows some cppreference points.
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <iostream>
// Optional v2: semi-serious attempt using cppref as a reference.
// NOTE: There are some caveats e.g not handling pointer types, etc.
namespace toy {
// BRIEF: placeholder for no value
struct MyNullopt {};
// BRIEF: runtime error inheriting from base exception as per cppref
class BadOptAccess : public std::exception
{
const char *what() const noexcept override
{
return "Bad optional access, no value.\n";
}
};
/// BRIEF: Optional's utility to store nothing or desired value
template <typename T>
struct StorageBase
{
// BRIEF: union to optionally contain a value of T if Optional is empty
union Mono
{
MyNullopt filler;
// NOTE: remove const / volatile qualifiers so I can store the actual data type
std::remove_cv_t<T> value;
} data;
// BRIEF: tracks state of Optional: "full" or empty
bool fill_flag;
constexpr StorageBase() noexcept : fill_flag{false} {}
constexpr StorageBase(MyNullopt blah) noexcept : fill_flag{false} {}
constexpr StorageBase(T &arg) noexcept : fill_flag{true}
{
data.value = arg;
}
constexpr StorageBase(T &&arg) noexcept : fill_flag{true}
{
data.value = arg;
}
// NOTE: I will use utility assign methods manually, so assignment overloads are N/A...
constexpr bool getFlag() const { return fill_flag; }
void clearFlag()
{
fill_flag = false;
}
const T& getValue() const
{
if (getFlag())
return data.value;
throw BadOptAccess{};
}
void assignValue(MyNullopt nul_arg)
{
data.filler = nul_arg;
clearFlag();
}
template <typename FwdTp>
void assignValue(FwdTp&& fw_arg)
{
using naked_tp = std::remove_reference_t<FwdTp>;
// NOTE: Mono data's active member is now that of stored type, fill_flag logic prevents UB access of other inactives
data.value = std::forward<naked_tp>(fw_arg);
fill_flag = true;
}
void resetValue() noexcept
{
if (getFlag())
data.value.~T();
clearFlag();
}
~StorageBase()
{
resetValue();
}
};
template <typename T>
class Optional : StorageBase<T>
{
private:
using tp_store_t = StorageBase<T>;
public:
constexpr Optional() noexcept : tp_store_t() {}
constexpr Optional(MyNullopt blah) noexcept : tp_store_t(blah) {}
constexpr Optional(const Optional<T>& other)
{
if (other.hasItem())
{
tp_store_t::assignValue(other.getValue());
return;
}
tp_store_t::assignValue(MyNullopt {});
}
constexpr Optional(Optional&& other) noexcept
{
if constexpr (other.hasItem())
{
tp_store_t::assignValue(std::move(other.getItem()));
return;
}
tp_store_t::assignValue(MyNullopt {});
}
template <typename Tp = T>
constexpr Optional(Tp&& arg) : tp_store_t(arg) {}
/// Assignment overloads
Optional& operator=(MyNullopt&& blah) noexcept
{
tp_store_t::assignValue(blah);
return *this;
}
Optional& operator=(const Optional<T>& other)
{
if (&other == this || (!hasItem() && !other.hasItem()))
return *this;
if (!other.hasItem())
resetSelf();
else
tp_store_t::assignValue(other.getItem());
return *this;
}
Optional& operator=(Optional<T>&& other)
{
if (!hasItem() || !other.hasItem())
return *this;
// NOTE: moved-from optional still contains a value unlike the commonly destructive moves- just copy to respect that. Not the best solution tbh
if (!other.hasItem())
resetSelf();
else
tp_store_t::assignValue(other.getItem());
return *this;
}
template <typename FwdType = T>
constexpr Optional& operator=(FwdType&& fw_arg)
{
tp_store_t::assignValue(fw_arg);
return *this;
}
/// Observers
constexpr bool hasItem() const
{
return tp_store_t::getFlag();
}
const T& getItem() const
{
return tp_store_t::getValue();
}
explicit operator bool() const noexcept { return hasItem(); }
/// Modifiers
void resetSelf() noexcept
{
tp_store_t::resetValue();
}
~Optional()
{
resetSelf();
}
};
} // namespace toy
template <typename OptTp>
using MyOpt = toy::Optional<OptTp>;
int main()
{
using MyOptNul = toy::MyNullopt;
using MyOptErr = toy::BadOptAccess;
MyOpt<int> maybe_num_1 {MyOptNul {}};
MyOpt<int> maybe_num_2 = 42;
MyOpt<int> maybe_num_3 {maybe_num_1};
// print test values...
std::boolalpha(std::cout);
std::cout << static_cast<bool>(maybe_num_1) << '\n'; // false
std::cout << maybe_num_1.hasItem() << '\n'; // false
try
{
std::cout << maybe_num_1.getItem() << '\n';
}
catch (const MyOptErr& e)
{
std::cout << "Nothing is in maybe_num_1!\n";
}
std::cout << static_cast<bool>(maybe_num_2) << '\n'; // true
std::cout << maybe_num_2.hasItem() << '\n'; // true
std::cout << maybe_num_2.getItem() << '\n'; // 42
maybe_num_2.resetSelf(); // should destroy its value and become empty
std::cout << maybe_num_2.hasItem() << '\n'; // false
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment