为了提高我对C++模板元编程、SFINAE、参考和总体类设计的理解,我尝试在C++中实现一个Maybe类。
当然,这个类在很大程度上是基于Haskell的Maybe Monad,并且具有相同的功能。我知道std::optional在C++17中几乎做了同样的事情,但我决定不按照标准所指定的那样实现它。
具体来说,我重命名了几个函数,并添加了我自己的一些函数(即apply方法)。更值得注意的是,我试图让它支持可能的引用(通过使用std::reference_wrapper)。
以下代码如下:
maybe.hpp:
#pragma once
#include
#include
#include
#include
namespace maybe {
struct nothing_t {} nothing;
template
class Maybe_Base {
public:
struct bad_access : std::exception {
virtual const char* what() const noexcept {
return "Attempted to access a value of a Maybe wrapper that doesn't exist.";
}
};
Maybe_Base():
val(nullptr)
{}
Maybe_Base(nothing_t):
val(nullptr)
{}
template
explicit Maybe_Base(Args&&... args) {
val = new T(std::forward(args)...);
}
~Maybe_Base() {
if (val)
val->~T();
delete val;
}
// Is there a way to ensure extraneous copies aren't made? Or is the only option just to delete the copy constructor entirely?
Maybe_Base(const Maybe_Base& other) {
if (other.val)
val = new T(*other.val);
else
val = nullptr;
}
Maybe_Base& operator=(Maybe_Base other) {
swap(*this, other);
return *this;
}
friend void swap(Maybe_Base& a, Maybe_Base& b) {
using std::swap;
swap(a.val, b.val);
}
Maybe_Base(Maybe_Base&& other) {
this->val = other.val;
other.val = nullptr;
}
inline bool empty() const {
return val == nullptr;
}
inline bool hasValue() const {
return !empty();
}
inline explicit operator bool() const {
return hasValue();
}
T value() {
if (empty())
throw bad_access();
else
return *val;
}
T valueOr(T defaultVal) {
if (empty())
return defaultVal;
return *val;
}
const T& operator*() const {
return *val;
}
T& operator*() {
return *val;
}
const T* operator->() const {
return val;
}
T* operator->() {
return val;
}
void clear() {
val->~T();
delete val;
val = nullptr;
}
template
std::enable_if_t< !std::is_void_v>
&& !std::is_member_function_pointer_v,
Maybe_Base>
>
apply(Func f, Args&&... args) {
if (*this)
return Maybe_Base>{f(this->value(), std::forward(args)...)};
return nothing;
}
template
std::enable_if_t< std::is_void_v>
&& !std::is_member_function_pointer_v,
void
>
apply(Func f, Args&&... args) {
if (*this)
f(this->value(), std::forward(args)...);
}
template
std::enable_if_t< !std::is_void_v>
&& std::is_member_function_pointer_v,
Maybe_Base>
>
apply(Pointer_To_Method f, Args&&... args) {
if (*this)
return Maybe_Base>{(this->value().*f)(std::forward(args)...)};
return nothing;
}
template
std::enable_if_t< std::is_void_v>
&& std::is_member_function_pointer_v,
void
>
apply(Pointer_To_Method f, Args&&... args) {
if (*this)
(this->value().*f)(std::forward(args)...);
}
private:
T* val;
};
template >
class Maybe;
template
class Maybe : public Maybe_Base {
public:
template
Maybe(Args&&... args): Maybe_Base(std::forward(args)...) {}
};
template
class Maybe : public Maybe_Base>> {
typedef std::reference_wrapper> Wrap_Type;
public:
template
Maybe(Args&&... args): Maybe_Base(std::ref(args)...) {}
T value() {
return Maybe_Base::value().get();
}
};
} // namespace maybe下面是一个小的测试.cpp文件,以确保实现工作正常:
#include
#include
#include
#include "maybe.hpp"
struct foo {
int i;
foo(int i_): i(i_) {}
foo() {}
void bar(int j) {
std::cout << "Bar." << ' ' << i*j << std::endl;
}
~foo() {
std::cout << "Deleting a foo..." << std::endl;
}
};
maybe::Maybe letter(int i) {
if (i >= 0 && i < 26)
return maybe::Maybe(i + 'A');
return maybe::nothing;
}
int main() {
maybe::Maybe> i{ std::make_unique(8) };
maybe::Maybe> j = std::move(i);
maybe::Maybe f{12321};
std::cout << std::boolalpha << j.hasValue() << ' ' << i.hasValue() << std::endl;
f.apply(&foo::bar, 2);
int p = 7;
maybe::Maybe q = p;
std::cout << q.value() << std::endl;
p = 8;
std::cout << q.value() << std::endl;
// Could I possibly do something simpler like `q = 6`?
q.value() = 6;
std::cout << p << std::endl;
q.apply([] (int a) { std::cout << a << std::endl; });
std::cout << q.apply([] (int a) { return a + 1; }).value();
maybe::Maybe c = letter(12);
std::cout << c.value() << std::endl;
}有什么事情我好像搞砸了,或者我忽略了什么情况?我特别关心的是确保实现是高效的,而且使用起来很容易。
发布于 2019-01-15 20:33:42
std::unique_ptr。(它比手动内存管理更安全、更容易)。delete操作符将调用对象析构函数。我们不应该手动调用析构函数。(这是一个非常好的例子,说明了为什么我们应该使用std::unique_ptr并完全避免这一点:D )。std::optional没有分配内存本身,而是将包含的对象保存在堆栈中。这可以用布尔标志和std::aligned_storage完成,也可以用std::variant完成。value和valueOr可以是const函数。value也许应该返回一个引用,并具有const和non版本。std::optional重载operator->和operator*。它们是没有必要的,而且不太明显它是什么类型。它不是指针类型(至少在语义上是这样),所以我认为它们没有意义。就我个人而言我会跳过它们。std::reference_wrapper,那么我们需要重新实现其他访问函数,而不仅仅是value()。目前,reference_wrapper是通过valueOr、operator*和operator->公开的。(这导致了“引用可能”的不同实现,我不知道这是否是一件好事。但是,我觉得我们要么完全隐藏std::reference_wrapper,要么让用户自己创建一个Maybe> (如果他们需要的话)。apply方法非常有趣。::) const版本(用于调用const Maybe‘S中包含的类型的const成员函数)。Maybe也可以存储简单的apply类型,其中D44函数没有意义,也许apply应该实现为一组空闲函数(并命名为invoke_maybe或类似的函数)。这将提供一个更符合std::invoke和std::bind的调用语法。它还允许为其他类实现invoke_maybe,如std::function (或std::optional),必要时返回一个空的Maybe。std::invoke允许我们访问成员变量,而不仅仅是成员函数。目前的apply实现似乎不支持这一点,但会非常酷。发布于 2019-01-16 16:54:04
坦率地说,我不确定它是否真的是一个Maybe实现。实际上,它与智能指针并没有太大的不同(嗯,也许没那么聪明,因为析构函数删除了底层值两次,如@ very 673679所指出的那样:)。顺便说一句,我不认为智能指针-or任何指针-是Haskell Maybe类型的一个糟糕的近似:它可以是nullptr (Nothing)或指向某个值(Just some_value)。当然,std::optional可能会更有效,因为它将可选值存储在堆栈上,但我不认为这是朝向Maybe的概念飞跃:一方面指针和std::optional的真正区别是Maybe是求和类型,它可以是不同类型的类型之一,而指针或std::optional是定义良好的空/空值类型。
在C++中实现和类型是相当困难的。标准库的sum类型- -std::variant- -相当麻烦,并且吸引了令人信服的投诉。但是,它也是一条获得比指针或可选指针更强大的途径:它们可以是Maybe的一个很好的近似,但不是Either,这一点根本上没有区别,而且更强大。
那么,Maybe作为一个sum类型在C++中会是什么样子?我想说的是:
#include
struct Nothing {};
template
struct Just {
Just(const T& t) : value(t) {}
Just(T&& t) : value(t) {}
Just() = default;
T value;
};
template
using Maybe = std::variant>;那你会怎么用呢?创建一个很像你所做的:
Maybe letter(int i) {
if (i >= 0 && i < 26) return Just('A' + i);
return Nothing();
}现在,您能用它做什么呢?Maybe是Haskell意义上的函子,所以您需要一种将函数映射到它上的方法。Haskell的签名是:(a -> b) -> F a -> F b。C++实现将遵循以下思路:
template
auto fmap(Fn fn, const Maybe& mb) {
using return_type = decltype(fn(std::declval()));
auto visitor = [fn](auto&& arg) -> Maybe
{
using Type = std::decay_t;
if constexpr(std::is_same_v) return Nothing();
else return Just(fn(arg.value));
};
return std::visit(visitor, mb);
/* visit is the *apply* you're looking for: given a visitor with
overloads for any type the variant can contain a value of, it
will apply the correct overload on the value it contains */
}现在也许也是一首单曲。如果您想实现>>= (又名bind),它在Haskell中的签名是(a -> M b) -> M a -> M b,那么它并没有什么不同:
template
auto bind(Fn fn, const Maybe& mb) {
using return_type = decltype(fn(std::declval()));
auto visitor = [fn](auto&& arg) -> return_type
{
using Type = std::decay_t;
if constexpr(std::is_same_v) return Nothing();
else return fn(arg.value);
};
return std::visit(visitor, mb);
}下面是一些代码片段的链接,如果您想探索这条静脉的话。
https://codereview.stackexchange.com/questions/211558
复制相似问题