Last active
June 25, 2024 22:11
-
-
Save kolayne/e4791ed1fd71824ced1a63f93354929a to your computer and use it in GitHub Desktop.
My attempts to implement a class similar to `std::optional`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <stdexcept> | |
template<class T> | |
class Maybe { | |
private: | |
struct _EmptyByte {}; | |
union _ { | |
_EmptyByte empty; | |
T payload; | |
// Must add a destructor here because `T` might have a non-trivial destructor and the compiler cannot generate | |
// the default destructor for this union (for it is impossible to decide whether to destruct `_EmptyByte` or | |
// `T`). | |
// So we explicitly declare an empty destructor and leave the actual payload to be destructed by the outer class | |
// if needed. | |
#if __cplusplus >= 202002L | |
constexpr // `constexpr` ctors/dtors are only supported starting with C++20 | |
#endif | |
~_() {} | |
} optional_payload{_EmptyByte{}}; // Initialized with empty byte by default | |
bool payload_contained = false; | |
public: | |
#if __cplusplus >= 202002L | |
constexpr | |
#endif | |
Maybe() noexcept = default; | |
#if __cplusplus >= 202002L | |
constexpr | |
#endif | |
explicit Maybe(const T &other) { | |
set(other); | |
} | |
#if __cplusplus >= 202002L | |
constexpr | |
#endif | |
~Maybe() noexcept { | |
reset(); | |
} | |
void reset() { | |
if (payload_contained) { | |
// When requested to reset the contained value, we need to destroy the internal object. There is no | |
// other way to do it than to call the destructor explicitly (you could have overwritten it with another | |
// value, which you'll see below, but here there is no other value) | |
optional_payload.payload.~T(); | |
payload_contained = false; | |
} | |
} | |
[[nodiscard]] inline bool contains_value() const noexcept { | |
return payload_contained; | |
} | |
T &get_value() { | |
if (!payload_contained) | |
throw std::runtime_error("Maybe is Nothing..."); | |
return optional_payload.payload; | |
} | |
// Note: | |
// 1. In order for `std::enable_if` to work, the below function must be a template, and the type being | |
// substituted in it must be a template parameter. | |
// 2. The problem that appears is that `template<class U = T>` only specifies the default value of the | |
// template parameter `U`, which means it may be overwritten explicitly by the user of my class. | |
// The solution of the standard library is to make the argument of this function have the type `U`, | |
// not `T`. Thus, if user tries to use `set<T1>(T2)` for some unrelated types `T1`, `T2`, this | |
// results in a compilation error (which is correct and quite expected), but this also brings in | |
// the ability to run things like `Maybe<float>.set<int>(int)`, as long as the conversions for the | |
// related types are declared. So it turns out to be a feature, doesn't it? | |
template<class U = T> | |
typename std::enable_if<std::__and_<std::is_copy_assignable<U>, std::is_copy_constructible<T>>::value>::type | |
set(const U &other) { | |
if (payload_contained) | |
// If payload is contained, we just assign the new value to our payload. The usual C++ behavior | |
// (copy assignment) happens. Nothing to worry about here - it's the responsibility of the original class's | |
// author to provide us with a good copy assignment operator | |
optional_payload.payload = other; | |
else { | |
// However, if there is no payload at the moment (e.g. we have been reset), we cannot perform the assignment | |
// because it will try to delete the old value which does not actually exist! Therefore, we run the | |
// constructor `T(const T &other)` in the memory `&optional_payload.payload`. This is called"Placement new". | |
// More details on it: https://en.cppreference.com/w/cpp/language/new#Placement_new | |
new(&optional_payload.payload) T(other); | |
payload_contained = true; | |
} | |
} | |
template<class U = T> | |
typename std::enable_if<std::__and_<std::is_move_assignable<U>, std::is_move_constructible<T>>::value>::type | |
set(U &&other) { | |
if (payload_contained) | |
optional_payload.payload = std::move(other); | |
else { | |
new(&optional_payload.payload) T(std::move(other)); | |
payload_contained = true; | |
} | |
} | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// You can run this code with `valgrind -s` to make sure everything is constructed/copied/destructed in a correct way | |
#include <iostream> | |
#include <vector> | |
#include "Maybe.cpp" | |
class NonCopyable { | |
public: | |
// Additional constraint: does not have a default constructor | |
NonCopyable() = delete; | |
explicit NonCopyable(int x) : x(x) {} | |
NonCopyable(const NonCopyable &other) = delete; | |
NonCopyable &operator=(const NonCopyable &other) = delete; | |
NonCopyable(NonCopyable &&other) = default; | |
NonCopyable &operator=(NonCopyable &&other) = default; | |
int x; | |
}; | |
int main() { | |
Maybe<std::vector<int>> mb; | |
std::vector<int> tmp1 = {1, 2, 3}; | |
mb.set(tmp1); | |
mb.set({4, 5, 6}); | |
mb.get_value().push_back(3); | |
mb.get_value().push_back(0); | |
std::cerr << mb.get_value().size() << '\n'; | |
mb.reset(); | |
Maybe<std::vector<int>> mb2({7, 8, 9}); | |
Maybe<NonCopyable> mb3; | |
NonCopyable x{0}; | |
//mb3.set(x); // attempt to copy: compilation error | |
mb3.set(std::move(x)); | |
mb3.set(NonCopyable{1}); | |
std::cerr << mb3.get_value().x << '\n'; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment