如何展平嵌套的std::optional?
How to flatten the nested std::optional?
注意:这个问题被简单地标记为 的重复,但它不是完全重复的,因为我专门询问 std::optionals。如果您关心一般情况,这仍然是一个很好的问题。
假设我有嵌套的可选值,像这样(愚蠢的玩具示例):
struct Person{
const std::string first_name;
const std::optional<std::string> middle_name;
const std::string last_name;
};
struct Form{
std::optional<Person> person;
};
和这个垃圾功能:
void PrintMiddleName(const std::optional<Form> form){
if (form.has_value() && form->person.has_value() && form->person->middle_name.has_value()) {
std::cout << *(*(*form).person).middle_name << std::endl;
} else {
std::cout << "<none>" << std::endl;
}
}
压平此可选检查的最佳方法是什么?
我做了这样的东西,它不是可变的,但我不太关心那个(如果真的有必要,我可以再添加一个级别(用 membr3
重载),除此之外的一切都是糟糕的代码) .
template<typename T, typename M>
auto flatten_opt(const std::optional<T> opt, M membr){
if (opt.has_value() && (opt.value().*membr).has_value()){
return std::optional{*((*opt).*membr)};
}
return decltype(std::optional{*((*opt).*membr)}){};
}
template<typename T, typename M1, typename M2>
auto ret_val_helper(){
// better code would use declval here since T might not be
// default constructible.
T t;
M1 m1;
M2 m2;
return ((t.*m1).value().*m2).value();
}
template<typename T, typename M1, typename M2>
std::optional<decltype(ret_val_helper<T, M1, M2>())> flatten_opt(const std::optional<T> opt, M1 membr1, M2 membr2){
if (opt.has_value() && (opt.value().*membr1).has_value()){
const auto& deref1 = *((*opt).*membr1);
if ((deref1.*membr2).has_value()) {
return std::optional{*(deref1.*membr2)};
}
}
return {};
}
void PrintMiddleName2(const std::optional<Form> form){
auto flat = flatten_opt(form, &Form::person, &Person::middle_name);
if (flat) {
std::cout << *flat;
}
else {
std::cout << "<none>" << std::endl;
}
}
备注:
- 我不想从
std::optional
切换到 some better optional。
- 我不太关心 perf,除非我 return 我必须复制一个指针(除非 arg 是临时的),因为
std::optional
不支持引用。
- 我不关心
flatten_has_value
函数(尽管它很有用),因为如果有一种方法可以很好地展平嵌套的可选值,那么也有一种方法可以编写该函数。
- 我知道我的代码看起来可以工作,但是它很丑,所以我想知道是否有更好的解决方案。
让我们看看展平函数的最佳形式是什么样的。在这种情况下,“最佳”是指最小的演示文稿。
即使在最佳情况下,在执行展平操作时,您也需要提供:
- 要展平的
optional<T>
对象。
- 展平操作函数名。
- 在每个扁平化步骤中按顺序间接列出的名称列表。
您的代码非常接近最佳。唯一的问题是“名称列表”中的每个名称都必须包含您在该级别访问的成员的类型名称,假设可以使用 T
.[=13= 的知识计算出该名称]
C++ 没有比这更好的机制。如果要访问对象的成员,则必须提供该对象的类型。如果你想间接地这样做,C++允许成员指针,但是获得这样的指针需要知道成员被提取时对象的类型。即使是 offsetof
体操也需要在获得偏移量时使用类型名称。
反射可以提供更好的东西,因为您可以传递编译时字符串,静态反射可以使用这些字符串从当前使用的类型中获取成员指针。但是C++20没有这样的特性。
对于本质上可链接的操作,您有很多辅助函数。 C++ 有用于链的东西:运算符。所以我可能会(ab)为此使用 operator*
。
对于您的具体情况,您只需要
template<class class_t, class member_t>
std::optional<std::remove_cv_t<member_t>> operator*(
const std::optional<class_t>& opt,
const std::optional<member_t> class_t::*member)
{
if (opt.has_value()) return opt.value().*member;
else return {};
}
void PrintMiddleName2(const std::optional<Form> form){
auto middle = form * &Form::person * &Person::middle_name;
if (middle) {
std::cout << *middle;
}
else {
std::cout << "<none>" << std::endl;
}
}
但实际上您可能还需要非可选成员的变体、getter 方法和任意转换,我已在此处列出,但我不能 100% 确定它们都能编译正确。
//data member
template<class class_t, class member_t>
std::optional<std::remove_cv_t<member_t>> operator*(const std::optional<class_t>& opt, const std::optional<member_t> class_t::*member) {
if (opt.has_value()) return opt.value().*member;
else return {};
}
template<class class_t, class member_t>
std::optional<std::remove_cv_t<member_t>> operator*(const std::optional<class_t>& opt, const member_t class_t::*member) {
if (opt.has_value()) return {opt.value().*member};
else return {};
}
//member function
template<class class_t, class return_t>
std::optional<std::remove_cv_t<return_t>> operator*(const std::optional<class_t>& opt, std::optional<return_t>(class_t::*member)()) {
if (opt.has_value()) return opt.value().*member();
else return {};
}
template<class class_t, class return_t>
std::optional<std::remove_cv_t<return_t>> operator*(const std::optional<class_t>& opt, return_t(class_t::*member)()) {
if (opt.has_value()) return {opt.value().*member()};
else return {};
}
//arbitrary function
template<class class_t, class return_t, class arg_t>
std::optional<std::remove_cv_t<return_t>> operator*(const std::optional<class_t>& opt, std::optional<return_t>(*transform)(arg_t&&)) {
if (opt.has_value()) return transform(opt.value());
else return {};
}
template<class class_t, class return_t, class arg_t>
std::optional<std::remove_cv_t<return_t>> operator*(const std::optional<class_t>& opt, return_t(*transform)(arg_t&&)) {
if (opt.has_value()) return {transform(opt.value())};
else return {};
}
您正在查找的操作称为单子 bind 操作,有时拼写为 and_then
(如 P0798 and Rust 中)。
您正在获取一个 optional<T>
和一个函数 T -> optional<U>
,并希望返回一个 optional<U>
。在这种情况下,该函数是指向数据成员的指针,但从这个意义上讲,它确实表现得像一个函数。 &Form::person
接受一个 Form
并返回一个 optional<Person>
.
您应该以与 种类 函数无关的方式编写此代码。事实上,它是一个指向成员数据的指针在这里并不重要,也许明天你会想要一个指向成员函数的指针,甚至是一个自由函数。那就是:
template <typename T,
typename F,
typename R = std::remove_cvref_t<std::invoke_result_t<F, T>>,
typename U = mp_first<R>>
requires SpecializationOf<R, std::optional>
constexpr auto and_then(optional<T> o, F f) -> optional<U>
{
if (o) {
return std::invoke(f, *o);
} else {
return std::nullopt;
}
}
这是众多用 C++ 编写起来很惨的函数声明之一,even with concepts。我将把它留作练习,以便在其中正确添加引用。我选择专门将它写成 -> optional<U>
而不是 -> R
因为我认为它对可读性很重要,你可以看到它实际上是 return 某种 optional
.
现在,问题是我们如何链接 this 到多个函数。 Haskell 使用 >>=
进行单子绑定,但在 C++ 中有错误的关联(o >>= f >>= g
会首先评估 f >>= g
并需要括号)。因此,下一个最接近的运算符选择是 >>
(这在 Haskell 中意味着不同的东西,但我们不是 Haskell,所以没关系)。或者您可以借用 Ranges 所做的 |
模型来实现此功能。
所以我们要么在语法上结束:
auto flat = form >> &Form::person >> &Person::middle_name;
或
auto flat = form | and_then(&Form::person)
| and_then(&Person::middle_name);
另一种将多个 monadic 绑定组合在一起的方法是 Haskell 拼写 >=>
的操作,称为 Kleisli 组合。在这种情况下,它接受一个函数 T -> optional<U>
和一个函数 U -> optional<V>
并生成一个函数 T -> optional<V>
。这是编写约束非常烦人的东西,所以我将跳过它,它看起来像这样(使用 Haskell 运算符拼写):
template <typename F, typename G>
constexpr auto operator>=>(F f, G g) {
return [=]<typename T>(T t){
using R1 = std::remove_cvref_t<std::invoke_result_t<F, T>>;
static_assert(SpecializationOf<R1, std::optional>);
using R2 = std:remove_cvref_t<std::invoke_result_t<G, mp_first<R1>>>;
static_assert(SpecializationOf<R2, std::optional>);
if (auto o = std::invoke(f, t)) {
return std::invoke(g, *o);
} else {
// can't return nullopt here, have to specify the type
return R2();
}
};
}
然后你可以写(或者至少你可以,如果 >=>
是你可以使用的运算符):
auto flat = form | and_then(&Form::person >=> &Person::middle_name);
因为 >=>
的结果现在是一个接受 Form
和 return 的函数 optional<string>
。
注意:这个问题被简单地标记为
假设我有嵌套的可选值,像这样(愚蠢的玩具示例):
struct Person{
const std::string first_name;
const std::optional<std::string> middle_name;
const std::string last_name;
};
struct Form{
std::optional<Person> person;
};
和这个垃圾功能:
void PrintMiddleName(const std::optional<Form> form){
if (form.has_value() && form->person.has_value() && form->person->middle_name.has_value()) {
std::cout << *(*(*form).person).middle_name << std::endl;
} else {
std::cout << "<none>" << std::endl;
}
}
压平此可选检查的最佳方法是什么?
我做了这样的东西,它不是可变的,但我不太关心那个(如果真的有必要,我可以再添加一个级别(用 membr3
重载),除此之外的一切都是糟糕的代码) .
template<typename T, typename M>
auto flatten_opt(const std::optional<T> opt, M membr){
if (opt.has_value() && (opt.value().*membr).has_value()){
return std::optional{*((*opt).*membr)};
}
return decltype(std::optional{*((*opt).*membr)}){};
}
template<typename T, typename M1, typename M2>
auto ret_val_helper(){
// better code would use declval here since T might not be
// default constructible.
T t;
M1 m1;
M2 m2;
return ((t.*m1).value().*m2).value();
}
template<typename T, typename M1, typename M2>
std::optional<decltype(ret_val_helper<T, M1, M2>())> flatten_opt(const std::optional<T> opt, M1 membr1, M2 membr2){
if (opt.has_value() && (opt.value().*membr1).has_value()){
const auto& deref1 = *((*opt).*membr1);
if ((deref1.*membr2).has_value()) {
return std::optional{*(deref1.*membr2)};
}
}
return {};
}
void PrintMiddleName2(const std::optional<Form> form){
auto flat = flatten_opt(form, &Form::person, &Person::middle_name);
if (flat) {
std::cout << *flat;
}
else {
std::cout << "<none>" << std::endl;
}
}
备注:
- 我不想从
std::optional
切换到 some better optional。 - 我不太关心 perf,除非我 return 我必须复制一个指针(除非 arg 是临时的),因为
std::optional
不支持引用。 - 我不关心
flatten_has_value
函数(尽管它很有用),因为如果有一种方法可以很好地展平嵌套的可选值,那么也有一种方法可以编写该函数。 - 我知道我的代码看起来可以工作,但是它很丑,所以我想知道是否有更好的解决方案。
让我们看看展平函数的最佳形式是什么样的。在这种情况下,“最佳”是指最小的演示文稿。
即使在最佳情况下,在执行展平操作时,您也需要提供:
- 要展平的
optional<T>
对象。 - 展平操作函数名。
- 在每个扁平化步骤中按顺序间接列出的名称列表。
您的代码非常接近最佳。唯一的问题是“名称列表”中的每个名称都必须包含您在该级别访问的成员的类型名称,假设可以使用 T
.[=13= 的知识计算出该名称]
C++ 没有比这更好的机制。如果要访问对象的成员,则必须提供该对象的类型。如果你想间接地这样做,C++允许成员指针,但是获得这样的指针需要知道成员被提取时对象的类型。即使是 offsetof
体操也需要在获得偏移量时使用类型名称。
反射可以提供更好的东西,因为您可以传递编译时字符串,静态反射可以使用这些字符串从当前使用的类型中获取成员指针。但是C++20没有这样的特性。
对于本质上可链接的操作,您有很多辅助函数。 C++ 有用于链的东西:运算符。所以我可能会(ab)为此使用 operator*
。
对于您的具体情况,您只需要
template<class class_t, class member_t>
std::optional<std::remove_cv_t<member_t>> operator*(
const std::optional<class_t>& opt,
const std::optional<member_t> class_t::*member)
{
if (opt.has_value()) return opt.value().*member;
else return {};
}
void PrintMiddleName2(const std::optional<Form> form){
auto middle = form * &Form::person * &Person::middle_name;
if (middle) {
std::cout << *middle;
}
else {
std::cout << "<none>" << std::endl;
}
}
但实际上您可能还需要非可选成员的变体、getter 方法和任意转换,我已在此处列出,但我不能 100% 确定它们都能编译正确。
//data member
template<class class_t, class member_t>
std::optional<std::remove_cv_t<member_t>> operator*(const std::optional<class_t>& opt, const std::optional<member_t> class_t::*member) {
if (opt.has_value()) return opt.value().*member;
else return {};
}
template<class class_t, class member_t>
std::optional<std::remove_cv_t<member_t>> operator*(const std::optional<class_t>& opt, const member_t class_t::*member) {
if (opt.has_value()) return {opt.value().*member};
else return {};
}
//member function
template<class class_t, class return_t>
std::optional<std::remove_cv_t<return_t>> operator*(const std::optional<class_t>& opt, std::optional<return_t>(class_t::*member)()) {
if (opt.has_value()) return opt.value().*member();
else return {};
}
template<class class_t, class return_t>
std::optional<std::remove_cv_t<return_t>> operator*(const std::optional<class_t>& opt, return_t(class_t::*member)()) {
if (opt.has_value()) return {opt.value().*member()};
else return {};
}
//arbitrary function
template<class class_t, class return_t, class arg_t>
std::optional<std::remove_cv_t<return_t>> operator*(const std::optional<class_t>& opt, std::optional<return_t>(*transform)(arg_t&&)) {
if (opt.has_value()) return transform(opt.value());
else return {};
}
template<class class_t, class return_t, class arg_t>
std::optional<std::remove_cv_t<return_t>> operator*(const std::optional<class_t>& opt, return_t(*transform)(arg_t&&)) {
if (opt.has_value()) return {transform(opt.value())};
else return {};
}
您正在查找的操作称为单子 bind 操作,有时拼写为 and_then
(如 P0798 and Rust 中)。
您正在获取一个 optional<T>
和一个函数 T -> optional<U>
,并希望返回一个 optional<U>
。在这种情况下,该函数是指向数据成员的指针,但从这个意义上讲,它确实表现得像一个函数。 &Form::person
接受一个 Form
并返回一个 optional<Person>
.
您应该以与 种类 函数无关的方式编写此代码。事实上,它是一个指向成员数据的指针在这里并不重要,也许明天你会想要一个指向成员函数的指针,甚至是一个自由函数。那就是:
template <typename T,
typename F,
typename R = std::remove_cvref_t<std::invoke_result_t<F, T>>,
typename U = mp_first<R>>
requires SpecializationOf<R, std::optional>
constexpr auto and_then(optional<T> o, F f) -> optional<U>
{
if (o) {
return std::invoke(f, *o);
} else {
return std::nullopt;
}
}
这是众多用 C++ 编写起来很惨的函数声明之一,even with concepts。我将把它留作练习,以便在其中正确添加引用。我选择专门将它写成 -> optional<U>
而不是 -> R
因为我认为它对可读性很重要,你可以看到它实际上是 return 某种 optional
.
现在,问题是我们如何链接 this 到多个函数。 Haskell 使用 >>=
进行单子绑定,但在 C++ 中有错误的关联(o >>= f >>= g
会首先评估 f >>= g
并需要括号)。因此,下一个最接近的运算符选择是 >>
(这在 Haskell 中意味着不同的东西,但我们不是 Haskell,所以没关系)。或者您可以借用 Ranges 所做的 |
模型来实现此功能。
所以我们要么在语法上结束:
auto flat = form >> &Form::person >> &Person::middle_name;
或
auto flat = form | and_then(&Form::person)
| and_then(&Person::middle_name);
另一种将多个 monadic 绑定组合在一起的方法是 Haskell 拼写 >=>
的操作,称为 Kleisli 组合。在这种情况下,它接受一个函数 T -> optional<U>
和一个函数 U -> optional<V>
并生成一个函数 T -> optional<V>
。这是编写约束非常烦人的东西,所以我将跳过它,它看起来像这样(使用 Haskell 运算符拼写):
template <typename F, typename G>
constexpr auto operator>=>(F f, G g) {
return [=]<typename T>(T t){
using R1 = std::remove_cvref_t<std::invoke_result_t<F, T>>;
static_assert(SpecializationOf<R1, std::optional>);
using R2 = std:remove_cvref_t<std::invoke_result_t<G, mp_first<R1>>>;
static_assert(SpecializationOf<R2, std::optional>);
if (auto o = std::invoke(f, t)) {
return std::invoke(g, *o);
} else {
// can't return nullopt here, have to specify the type
return R2();
}
};
}
然后你可以写(或者至少你可以,如果 >=>
是你可以使用的运算符):
auto flat = form | and_then(&Form::person >=> &Person::middle_name);
因为 >=>
的结果现在是一个接受 Form
和 return 的函数 optional<string>
。