Igor G
Igor G

Reputation: 2471

Can I have a std::optional<T> if T is neither constructible nor copyable nor movable?

The only requirement to type parameter of std::optional<T> class template mentioned in [optional.optional.general] p3 is that type T is Destructible.

Suppose I have a very restrictive class which meets that requirement:

struct R
{
    R(const R&) = delete;
    R& operator=(const R&) = delete;
    R(R&&) = delete;
    R& operator=(R&&) = delete;

    // This works thanks to guaranteed copy elision since C++17.
    static R create()
    {
        return R();
    }

  private:
    R() = default;
};

Yet that class is still perfectly usable:

void test_1()
{
    R obj = R::create();
    /* use obj */
}

The question is: can I really have a usable object of type std::optional<R>? By "usable" I mean an object that contains value. If yes, then how do I construct that value there?

The following code obviously fails because every constructor of R is inaccessible:

void test_2()
{
    std::optional<R> o(R::create());   // Error: no matching constructor
}

Godbolt example link.


Edit:

Wouldn't it be nice if std::optional had a constructor that accepted a builder?

template <typename Builder>
optional::optional(disabmiguating_tag, Builder f)
 : my_internal_union(f())
{
}

with apropriate constructor of my_internal_union of course...

Upvotes: 14

Views: 1044

Answers (2)

Jan Schultke
Jan Schultke

Reputation: 39869

You can use a a type with an operator R:

struct Builder {
    operator R() const { return R::create(); }
};

int main() {
    std::optional<R> o(Builder{});
}

See live example at Compiler Explorer

This works because the std::optional<R> o is constructed via the constructor #8 on cppreference, which means that the R that inhabits o is constructed as if by calling

R(std::move(builder))

... where builder is a reference to the Builder{} we have passed in.

operator R returns a prvalue, so the returned R::create() and the constructed R are the same object, by virtue of C++17 guaranteed copy elision.

Upvotes: 23

Enlico
Enlico

Reputation: 28490

I know the question is , but apparently you can use this trick:

auto o = std::optional<int>{0}.transform([](int){ return R::create(); });

where we create a dummy non-empty optional (in this case std::optional<int>) and use std::optional<T>::transform to apply a function on its inner int; that function returns the desired R object via R::create(), which is created in place in the memory that std::optional keeps reserved for its payload.

It is valid because of C++17's mandatory copy elision, because the lambda return type is R, which is the same type as R::create(), which in turn is prvalue. I think that means that this R object is constructed directly in the "final" place, which is inside the std::optional<R> o that you are constructing.


Probably std::construct_at from is the core of the trick, but I don't think you can use it from the outside, because you don't have access to std::optional's member where the payload is stored, and you can't access to it via .value() because that's UB on an empty std::optional, and you can't have a non-empty std::optional<R> because that's precisely what we are discussing!

Upvotes: 8

Related Questions