Skip to content

Conversation

@dangelog
Copy link
Contributor

These commits implement some miscellanea fixes for beman::optional. I'm basing them on P2988R12.

I think the dangling case is a real defect in the specification and deserves a discussion in LWG. The others are just implementation fixes.

If *this is an rvalue reference, we're required to move the value
contained into the function passed to transform(). Do that.
@dangelog dangelog requested review from a team, RaduNichita and wusatosi June 24, 2025 15:12
@coveralls
Copy link

coveralls commented Jun 24, 2025

Pull Request Test Coverage Report for Build 16011929600

Details

  • 11 of 11 (100.0%) changed or added relevant lines in 1 file are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage remained the same at 100.0%

Totals Coverage Status
Change from base Build 16007560103: 0.0%
Covered Lines: 311
Relevant Lines: 311

💛 - Coveralls

Copy link
Member

@neatudarius neatudarius left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but also wait for @steve-downey review

Copy link
Member

@steve-downey steve-downey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry not to take a look at this sooner.

I want to make sure I understand the changes before merging, particularly the dangling issue.

Do you have an NB comment drafted to ask for a fix to the spec if we need it? If so, I can copy/paste to my list to go to INCITS.

Also it looks like a number of my questions are answered in the individual commit messages, so I'll be re-reading those.

(marking "request changes" so I have time to answer my own questions. I'm not expecting @dangelog to do more work)

EXPECT_TRUE(o38r);
EXPECT_TRUE(*o38r == 42);

// transform and return a non-movable class
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are the tests I missed for transform and the reason for the from_function tag?

Do existing implementations get this right? Do we have a spec bug, or is silence enough to require it to work?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, the spec says "[Note 1: There is no requirement that U is movable ([dcl.init.general]).
— end note]" in https://eel.is/c++draft/optional#monadic-note-1

Implementations support such non-movable payloads: https://gcc.godbolt.org/z/Wh6q7EGsj

@dangelog
Copy link
Contributor Author

dangelog commented Jul 1, 2025

Hi! :-)

Sorry not to take a look at this sooner.

I want to make sure I understand the changes before merging, particularly the dangling issue.

Sure thing, I think I've explained in the associated commit message; basically I'm afraid that doing *value_ when value_ is a dangling dangling is instantly UB per https://eel.is/c++draft/expr.unary.op#1.sentence-4 .

(Since then *value_ yields a T& and or_else returns a optional<T&>, this will go through the optional<T&>::optional(U&&) constructor to create the return value.)

Instead we could just copy *this -- go through optional<T&>'s copy constructor. Formally, that copies the value_ pointer, and copying a dangling pointer is still implementation-defined (https://eel.is/c++draft/basic.compound#4), but that still sounds a better approach.

At least that's my understanding of the matter...

Do you have an NB comment drafted to ask for a fix to the spec if we need it? If so, I can copy/paste to my list to go to INCITS.

Is the wording for optional<T&> from the paper already merged? I think I can raise it as a LWG issue, together with the other couple of minor edits. Or would you prefer to file this as NB comment?

Also it looks like a number of my questions are answered in the individual commit messages, so I'll be re-reading those.

(marking "request changes" so I have time to answer my own questions. I'm not expecting @dangelog to do more work)

I'm happy to help and contribute a little!

@steve-downey
Copy link
Member

steve-downey commented Jul 1, 2025 via email

@steve-downey
Copy link
Member

steve-downey commented Jul 1, 2025 via email

dangelog added 2 commits July 2, 2025 00:57
This is supposed to work and it's explicitly spelled out in the standard
text.

If the function called by transform returns a non-movable prvalue,
we cannot rely on optional(T&&) constructor for this, because that will
not simply materialize it into the payload of the returned optional.

Instead, add a new private constructor for optional with a private tag.
Inside that constructor, we invoke the function and use it to initialize
the payload; this will not require any moves due to guaranteed copy
elision. In transform(), we call that constructor, passing the function
and the value stored in *this to it.

A similar change is needed for the optional<T&>.

Since we need to call this private optional<U>'s constructor from a
generic optional<T>::transform, we need to grant friendship to all
possible optional specializations.
If or_else is called on an engaged optional, we're supposed to return a
copy (or a move) of the `*this` object; otherwise we invoke the argument
of or_else, and return whatever optional that returns.

For optional<T> we were doing exactly that (`return *this` or
`move(*this)`). For optional<T&> we were instead doing `return *value_`,
where `value_` is the pointer used in the implementation. That ends up
creating an optional<T&> through its "value constructor".

The problem is that the two forms are not equivalent in corner cases;
consider this code:

```
T *obj = new T;
T &ref = *obj;
delete obj;     // obj now dangles

T &ref2 = ref;  // OK
```

The last line is OK even if `ref` does not refer to an object any more.

This code is instead not OK:

```
T *obj = new T;
T &ref = *obj;
delete obj;

T &ref2 = *obj; // UB, https://eel.is/c++draft/expr.unary.op#1
```

If we use optional<T&>::or_else, the implementation is isomorphic to the
second form, not to the first one:

```
T *obj = new T;
optional<T &> ref = *obj;
delete obj;

assert(ref); // OK
optional<T &> ref2 = ref.or_else(/* ... */); // UB; does *obj internally
```

We can avoid this UB by avoiding the dereference into
optional<T&>::or_else, and returning a copy of *this instead. The
semantics are otherwise the same, but we avoid tripping into UB.

I'm adding a test which however is inconclusive because compilers do
not detect the above UB during constant evaluation, although they're
required to do so. That's likely a bug.
@dangelog dangelog force-pushed the P2988R12_misc_fixes branch from a1ba572 to 6b5137f Compare July 1, 2025 22:57
@JeffGarland
Copy link
Member

JeffGarland commented Jul 2, 2025

At least that's my understanding of the matter... Do you have an NB comment drafted to ask for a fix to the spec if we need it? If so, I can copy/paste to my list to go to INCITS. Is the wording for optional<T&> from the paper already merged? I think I can raise it as a LWG issue, together with the other couple of minor edits. Or would you prefer to file this as NB comment?
If it hasn't been merged, it will be soon. A Library issue is always good, but an NB comment saying apply the fix for an issue makes sure it's done at Kona.

Please file an LWG issue here - this is always the best way: https://cplusplus.github.io/LWG/lwg-active.html#submit_issue

It will get attention without an NB comment (likely as P1 since it hasn't shipped), but if someone wants the NB comment can become fix LWG whatever-the-number. Since it's likely you'll have a resolution LWG can handle it promptly via list review without even going to NB route or even Kona. I expect we will do significant issue processing prior to Kona.

@steve-downey steve-downey dismissed their stale review July 8, 2025 07:55

Because I'm now satisfied I understand the changes here.

std::destroy_at(std::addressof(value_));
engaged_ = false;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewing for creating LWG issues:
I believe this, which is necessary for https://eel.is/c++draft/optional.monadic#note-1, is already covered by the current wording.

static_assert(std::is_same_v<std::remove_cvref_t<U>, optional>, "Result must be an optional");
if (has_value()) {
return *value_;
return *this;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the LWG issue fodder. We wish to change from constructing a new optional over the deferenced val, to using the converting constructor for optional from optional, in order to avoid the deref of a possibly dangling pointer and instead copy the value of the pointer. This may not be safe on all platforms, but is safer and defined on many.

@steve-downey steve-downey merged commit 0195cf3 into bemanproject:main Jul 8, 2025
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants