Converting one std::optional to another std::optional
Asked Answered
J

4

5

I have a method that returns an optional struct, like this:

auto getBook(const std::string &title) const -> std::optional<Book>;

I want to call this method in another method that returns the optional author. Problem is that the implementation should always check whether the optional returned by getBook is filled in before a method can be called, like this:

auto getAuthor(const std::string &title) const -> std::optional<Author>
{
   const auto optBook = getBook(title);
   if (optBook.has_value)
      return optBook->getAuthor();
   else
      return std::nullopt;
}

Is there a way to write this in a shorter way, so that if the optional is filled the method is called, but if the optional is empty, std::nullopt is returned. Something like this (I know this currently doesn't work but you get my point):

auto getAuthor(const std::string &title) const -> std::optional<Author>
{
   return getBook(title).getAuthor();
}
Jaipur answered 23/10, 2020 at 12:44 Comment(2)
There's nothing in C++ that will do this. The reason it doesn't work is due to core, fundamental way that C++ works. If you find that you have to do this sort of thing frequently, you'll have to implement your own scaffolding that implements it, probably using a template function of some kind.Assoil
Syntactic sugar for usecases like these have been introduced in languages like Swift and Rust, but not in C++ yet (and I don't believe we will see such things anytime soon)Illmannered
C
9

You can generalize this pattern by creating a map function, that takes an optional o and a function f, and returns the result of f(*o) if o.has_value() == true:

template <typename O, typename F>
auto map(O&& o, F&& f) -> std::optional<decltype(f(*std::forward<O>(o)))>
{
    if (!o.has_value()) 
    {
        return {std::nullopt};
    }

    return {f(*std::forward<O>(o))};
}

You can then define getAuthor as:

auto getAuthor(const std::string& title) -> std::optional<Author>
{
    return map(getBook(title), [](Book b)
    {
        return b.author;
    });
}

live example on godbolt.org


I made a library for these sort of operations, called scelta. With my library, you can write:

auto getBook(const std::string& title) -> std::optional<Book>;
auto getAuthor(const std::optional<Book>& title) -> std::optional<Author>;

using namespace scelta::infix;
std::optional<Author> a = getBook("...") | map(getAuthor);

See "Monadic Optional Operations" for more info.

Conscious answered 23/10, 2020 at 12:47 Comment(0)
B
1

boost::optional, which is what std::optional is based on, has a member map that does this.

E.g.

auto getBook(const std::string &title) const -> boost::optional<Book>;

auto getAuthor(const std::string &title) const -> boost::optional<Author>
{
   return getBook(title).map(std::mem_fn(&Book::getAuthor));
}
British answered 23/10, 2020 at 13:21 Comment(0)
C
0

I think, Null Object pattern is required here for the job instead of std::optional.

Yeah, it resembles std::optional a bit, but with a different semantics: When any method (like getAuthor) is called on an object, if method's result is valid, valid object is returned. If not, null object is returned instead.

Not too much time ago I worked on a XML-like tree implementation, it was something like that:

auto val = object.node("root").node("branch_1").node("subbranch_1").toInt();
if (val) process(val.asInt());

On every step of node() call, if specific node wasn't found, invalid node is returned. When it is called on an invalid node, invalid node is returned.

AFAIK, nothing like that is currently implemented in STL.

EDIT: for this to effectively work, you need all instances of books, authors, chapters, and other stuff to be derived from some base class, one that null object will be of.

EDIT: This isn't the best solution for all cases, probably, this is too complicated to implement in yours.

Castilian answered 23/10, 2020 at 13:22 Comment(0)
C
-2

I think you could just return what you want and catch the exceptions in root function, something like this:

auto  get_size_t(const bool state)
{
 return state ? std::optional<size_t>(std::rand()) : std::nullopt;
}

auto  get_string_size_t(const bool state)
{
 return std::optional<std::string>(std::to_string(get_size_t(state).value()));
}

void  f()
try
{
 std::clog << get_string_size_t(true).value() << std::endl;
 std::clog << get_string_size_t(false).value() << std::endl;
}
catch (...)
{
}

Closestool answered 23/10, 2020 at 13:2 Comment(2)
You should not use exception handling in this manner. But if you do, at least use catch (const std::bad_optional_access&) instead of catch(...)Mull
I just make a example of my solution.Closestool

© 2022 - 2025 — McMap. All rights reserved.