Can I copy-elide an immovable & uncopyable function result into an optional?
Asked Answered
O

3

28

I want to store a non-trivial type that is immovable and not copyable in an std::optional. However the object is constructed by a free function. (example)

struct Foo {
    Foo();
    Foo(Foo const&) = delete;
    Foo(Foo&&) = delete;
    Foo& operator=(Foo const&) = delete; // added for completeness
    Foo& operator=(Foo&&) = delete; // added for completeness
    ~Foo();
};
Foo foo();

Without changing Foo or foo();

Thanks to copy elision, I can already do this:

Foo f1 = foo();

And this also compiles, because std::optional only requires the stored type to be destructible:

std::optional<Foo> f2;
f2.emplace();

But I cannot fill f2 with a function result:

f2 = foo(); // no
f2.emplace(foo()); // no

Obviously, because this would require copies or moves of Foo. This is likely impossible, but have I overlooked something?

Opheliaophelie answered 10/1, 2023 at 9:23 Comment(0)
N
30

You can make a class where a conversion operator calls the function and returns the result. Since the conversion operator will create a prvalue, you don't need the type to be copyable/movable:

template<typename F>
struct call_wrapper {
    F&& f;

    constexpr operator decltype(auto)() && {
        return static_cast<F&&>(f)();
    }
};
template<typename F>
call_wrapper(F&&) -> call_wrapper<F>;


std::optional<Foo> f2;
f2.emplace(call_wrapper{foo});
Negotiant answered 10/1, 2023 at 9:59 Comment(5)
Here's a live demo. Seems to work well.Opheliaophelie
Why static_cast<F&&>? Seems to work without it: godbolt.org/z/1nKP513q4Interpolate
@SolomonUcko Perfect forwarding doesn't hurt. You can imagine a situation where the call needs to be on an rvalue (Like with call_wrapper itself because is meant to only be called once) or maybe an rvalue call is more efficient (std::move(f)() stealing from f)Negotiant
Note that the rules that govern such an elision are not fully specified; CWG 2327Capelin
I'd use std::forward<F>(f)() instead of static_cast<F&&>(f)(). The effect is identical, but it makes it clear you are doing perfect forwarding. Second, note that this technique doesn't support all types -- the type cannot have a constructor that accepts call_wrapper<F>. This happens for some ill-behaved types, like C++11's std::function (later versions behave better).Eldwun
V
15

We can take advantage of transform (new in C++23), which supports guaranteed elision:

auto f = std::optional(1).transform([](int) { return foo(); });

This is slightly more general than call_wrapper because it works even in cases where Foo would constructible from call_wrapper (because, for instance, it has a converting constructor template that accepts everything).

Virtuoso answered 10/1, 2023 at 21:24 Comment(1)
Yes, I think this would be the way to go for C++23.Opheliaophelie
I
6

Not sure if this fits your requirement of not modifying Foo or foo, but you can use a Bar wrapper that default initializes a Foo member by calling foo:

#include <optional>

namespace {
    struct Foo {
        Foo() = default;
        Foo(Foo const&) = delete;
        Foo(Foo&&) = delete;
        Foo& operator=(Foo const&) = delete;
        Foo& operator=(Foo&&) = delete;
        ~Foo() {}
    };
    Foo foo() { return {}; }

    struct Bar {
        Foo f = foo();
    };
}

int main() {
    std::optional<Bar> f3;
    f3.emplace();
}

I think the answer to the title is No. You cannot copy or move a Foo into an optional, but via the wrapper you can construct it in place.

Imogen answered 10/1, 2023 at 9:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.