Took a shot at implementing a subset of std::optional
functionality. A lot of core features are there but some things like converting constructors, etc are missing as I just wanted to focus on the basic ideas.
The implementation:
#include <compare>
#include <typeinfo>
#include <utility>
struct BadOptionalAccess : std::exception {
const char* what() const noexcept override {
return "Tried to access an Optional()'s value, but no value exists!";
}
};
template<typename T>
class Optional {
public:
Optional() noexcept {
std::memset(m_data, 0, sizeof(T));
}
Optional(const T& value) :
m_has_value{true} {
new (m_data) T(value);
}
template<typename... Args>
Optional(std::in_place_t, Args&&... args) {
emplace(std::forward<Args>(args)...);
}
Optional(const Optional& other) :
m_has_value{other.m_has_value} {
if (other.m_has_value) {
new(m_data) T(other.value());
}
}
Optional(Optional&& other) noexcept :
m_has_value{other.m_has_value} {
if (other.has_value()) {
new(m_data) T(std::move(other.value()));
other.m_has_value = false;
}
}
Optional& operator=(const Optional& other) {
Optional temp {other};
temp.swap(*this);
return *this;
}
Optional& operator=(Optional&& other) noexcept {
Optional temp {std::move(other)};
temp.swap(*this);
return *this;
}
auto operator<=>(const Optional& other) const noexcept {
if (!has_value() && !other.has_value()) {
return std::strong_ordering::equal;
} else if (!has_value() && other.has_value()) {
return std::strong_ordering::less;
} else if (has_value() && !other.has_value()) {
return std::strong_ordering::greater;
} else {
return value() <=> other.value();
}
}
bool operator==(const Optional& other) const noexcept {
return (*this <=> other) == std::strong_ordering::equal;
}
bool operator!=(const Optional& other) const noexcept {
return !(*this == other);
}
explicit operator bool() const noexcept {
return has_value();
}
template<typename... Args>
void emplace(Args&&... args) {
reset();
new (m_data) T(std::forward<Args>(args)...);
m_has_value = true;
}
template<typename U>
T value_or(U&& default_value) {
return has_value() ? std::move(**this)
: static_cast<T>(std::forward<U>(default_value));
}
void reset() {
if (m_has_value) {
T* val = reinterpret_cast<T*>(m_data);
val->~T();
m_has_value = false;
}
}
void swap(Optional& other) noexcept {
// Do I need to do this? My first instinct was to do
// an ordinary swap of the m_data's, but it seemed wrong
// to do in case the types weren't trivially copyable/movable.
// Maybe I should special case in those situations?
if (other.has_value() && has_value()) {
std::swap(*reinterpret_cast<T*>(m_data), *reinterpret_cast<T*>(other.m_data));
} else if (other.has_value()) {
new (m_data) T(std::move(other.value()));
} else if (has_value()) {
new(other.m_data) T(std::move(value()));
}
std::swap(m_has_value, other.m_has_value);
}
bool has_value() const noexcept {
return m_has_value;
}
const T& value() const {
return *ptr();
}
const T& operator*() const& {
return value();
}
const T* operator->() const {
return ptr();
}
T& value() {
return *ptr();
}
T& operator*() & {
return value();
}
T&& operator*() && noexcept {
return std::move(value());
}
const T&& operator*() const&& noexcept {
// not 100% sure how to use/test this overload. Why would an rvalue be const?
return std::move(**this);
}
T* operator->() {
return ptr();
}
~Optional() {
reset();
}
private:
alignas(T) unsigned char m_data[sizeof(T)];
bool m_has_value = false;
const T* ptr() const {
if (!m_has_value) {
throw BadOptionalAccess();
}
return (reinterpret_cast<const T*>(m_data));
}
T* ptr() {
if (!m_has_value) {
throw BadOptionalAccess();
}
return (reinterpret_cast<T*>(m_data));
}
};
template<typename T, typename... Args>
Optional<T> makeOptional(Args&&... args) {
return Optional<T>(std::in_place, std::forward<Args>(args)...);
}
Some tests:
#define BOOST_TEST_MODULE optionaltest
#ifdef BOOST_TEST_DYN_LINK
#include <boost/test/unit_test.hpp>
#else
#include <boost/test/included/unit_test.hpp>
#endif // BOOST_TEST_DYN_LINK
#include <boost/test/data/monomorphic.hpp>
#include <boost/test/data/test_case.hpp>
#include "Optional.h"
#include <string>
BOOST_AUTO_TEST_CASE(default_constructor_test) {
Optional<int> o;
}
BOOST_AUTO_TEST_CASE(basic_constructors_test) {
Optional<int> o1;
Optional<int> o2 = 1;
Optional<int> o3 = o2;
// calls std::string( size_type count, CharT ch ) constructor
Optional<std::string> o4(std::in_place, 3, 'A');
Optional<std::string> o5 = makeOptional<std::string>(3, 'A');
BOOST_CHECK(o4==o5);
// Move-constructed from std::string using deduction guide to pick the type
Optional o6(std::string{"deduction very long type"});
}
BOOST_AUTO_TEST_CASE(access_operator_test) {
using namespace std::string_literals;
Optional<int> opt1 = 1;
BOOST_CHECK_EQUAL(*opt1, 1);
*opt1 = 2;
BOOST_CHECK_EQUAL(*opt1, 2);
Optional<std::string> opt2 = "abc"s;
BOOST_CHECK_EQUAL(*opt2, "abc"s);
BOOST_CHECK_EQUAL(opt2->size(), 3);
Optional<std::string> taken = std::move(opt2);
BOOST_CHECK_EQUAL(*taken, "abc"s);
BOOST_CHECK_EQUAL(taken->size(), 3);
BOOST_CHECK(!opt2.has_value());
}
BOOST_AUTO_TEST_CASE(value_check_test) {
Optional<int> opt;
BOOST_CHECK(!opt.has_value());
opt = 43;
BOOST_CHECK(opt.has_value());
if (opt) {
BOOST_CHECK(true);
} else {
BOOST_CHECK(false);
}
opt.reset();
BOOST_CHECK(!opt.has_value());
}
BOOST_AUTO_TEST_CASE(get_value_test) {
Optional<int> opt = {};
BOOST_CHECK_THROW(opt.value(), BadOptionalAccess);
BOOST_CHECK_THROW(opt.value() = 42, BadOptionalAccess);
opt = 43;
BOOST_CHECK_EQUAL(*opt, 43);
BOOST_CHECK_EQUAL(opt.value(), 43);
opt.value() = 44;
BOOST_CHECK_EQUAL(*opt, 44);
BOOST_CHECK_EQUAL(opt.value(), 44);
}
BOOST_AUTO_TEST_CASE(ValueOrTest) {
// Test with existing value
Optional<int> opt1(5);
BOOST_CHECK_EQUAL(opt1.value_or(10), 5);
// Test without an existing value
Optional<int> opt2; // Empty optional
BOOST_CHECK_EQUAL(opt2.value_or(10), 10);
// Test with a different type
Optional<double> opt3; // Empty optional
BOOST_CHECK_CLOSE(opt3.value_or(10), 10.0, 0.0001); // BOOST_CHECK_CLOSE for floating point comparison
// Test with rvalue when Optional has a value
Optional<std::string> opt4("Hello");
BOOST_CHECK_EQUAL(opt4.value_or(std::string("World")), "Hello");
// Test with rvalue when Optional does not have a value
Optional<std::string> opt5; // Empty optional
BOOST_CHECK_EQUAL(opt5.value_or(std::string("World")), "World");
}
BOOST_AUTO_TEST_CASE(swap_test) {
Optional<std::string> opt1("Lorem ipsum dolor sit amet, consectetur tincidunt.");
Optional<std::string> opt2("Some other lorem ipsum");
opt1.swap(opt2);
BOOST_CHECK_EQUAL(*opt2, "Lorem ipsum dolor sit amet, consectetur tincidunt.");
BOOST_CHECK_EQUAL(*opt1, "Some other lorem ipsum");
opt1.reset();
opt1.swap(opt2);
BOOST_CHECK_EQUAL(*opt1, "Lorem ipsum dolor sit amet, consectetur tincidunt.");
BOOST_CHECK(!opt2.has_value());
}
BOOST_AUTO_TEST_CASE(comparison_test) {
Optional<int> o1, o2;
BOOST_CHECK(o1 == o2);
o2.emplace(10);
BOOST_CHECK(o1 < o2);
BOOST_CHECK(o2 > o1);
BOOST_CHECK(o2 != o1);
o1.emplace(10);
BOOST_CHECK(o1 == o2);
BOOST_CHECK(o1 <= o2);
BOOST_CHECK(o1 >= o2);
}
```
union{T;}
to represent the value, you won't need toreinterpret_cast
so much. See here: stackoverflow.com/a/65441992/7328782 \$\endgroup\$