Skip to content

Instantly share code, notes, and snippets.

@kolayne
Last active April 9, 2022 09:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kolayne/e4791ed1fd71824ced1a63f93354929a to your computer and use it in GitHub Desktop.
Save kolayne/e4791ed1fd71824ced1a63f93354929a to your computer and use it in GitHub Desktop.
My attempts to implement a class similar to `std::optional`
#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 then 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;
}
}
};
// 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