Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

When moving a 'optional<T&>', don't move the remote value #93

Merged

Conversation

jiixyj
Copy link
Contributor

@jiixyj jiixyj commented Dec 31, 2024

Moving optional<T&> into optional<T> should probably not result in a move of the remote value. Because of reference collapsing, the constraints of the move constructor/assignment operator of optional<T> already check for a copy of the remote value, not a move.

This makes the code match the constraints by moving the move before the dereference, since for optional<T&>, *std::move(rhs) always results in a lvalue reference, not a rvalue reference. introducing new constructor/assignment overloads for optional<T&>.

edit: fixes #92
edit: updated for new approach

@coveralls
Copy link

coveralls commented Dec 31, 2024

Coverage Status

coverage: 93.22% (+0.1%) from 93.091%
when pulling e5fab32 on jiixyj:shallow-value-category-on-move
into 5460182 on bemanproject:main.

// Moving an `optional<T&>` should not move the remote value.
// Check this by deleting the rvalue overloads of some (unrealistic) types.
TEST(OptionalTest, MoveRef) {
struct copyable_but_not_movable {
Copy link
Member

Choose a reason for hiding this comment

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

same obersation - we have a header file with test classes in src/beman/optional26/tests/test_types.hpp

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the pointer! I'll update the PRs accordingly.

@@ -488,7 +488,7 @@ inline constexpr optional<T>::optional(optional<U>&& rhs)
requires detail::enable_from_other<T, U, U&&> && std::is_convertible_v<U&&, T>
{
if (rhs.has_value()) {
Copy link
Member

Choose a reason for hiding this comment

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

https://eel.is/c++draft/optional#ctor-34 means this needs a paper.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, and there is https://eel.is/c++draft/optional#assign-25 as well.

Although in a world where only optional<T> exists I believe std::move(*rhs) and *std::move(rhs) do the same thing. So implementers could have used the as-if rule this whole time. Only for optional<T&> there might be a semantic difference. Maybe P2988 is a good vehicle for this?

@@ -868,3 +868,42 @@ TEST(OptionalTest, HashTest) {
EXPECT_EQ(h1, h2);
}
}

Copy link
Member

Choose a reason for hiding this comment

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

tl::optional and boost::optional both fail to compile this, so it's not a problem unique to beman::optional.

With beman::optional, this also fails to compile. The title of the PR lead me to think this was doing the wrong thing successfully. Do you have a case where moving an optional<T&> incorrectly moves the referred to value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With beman::optional, this also fails to compile. The title of the PR lead me to think this was doing the wrong thing successfully. Do you have a case where moving an optional<T&> incorrectly moves the referred to value?

Yes, I have a more realistic example at the issue: #92

@steve-downey
Copy link
Member

steve-downey commented Jan 2, 2025 via email

@jiixyj
Copy link
Contributor Author

jiixyj commented Jan 2, 2025

If it's that a optional will steal the referred to of optional<Foo&> in the converting assignment, it might be safer to specialize the assignment operator? If U is a reference it should always copy assign regardless of the value category of the optional.

This sounds sensible to me! It occured to me that the constraints are slightly wrong even for the optional(const optional<U>& rhs) converting constructor when called with a optional<U&>, so having some overloads for optional<T&> would be more correct. Did you have something like the following in mind?

    template <class U>
    constexpr explicit(!std::is_convertible_v<U, T>) optional(const optional<U>& rhs)
        requires(!std::is_reference_v<U> && detail::enable_from_other<T, U, const U&> && std::is_convertible_v<const U&, T>);
    template <class U>
    constexpr explicit(!std::is_convertible_v<U, T>) optional(const optional<U>& rhs)
        requires(!std::is_reference_v<U> && detail::enable_from_other<T, U, const U&> && !std::is_convertible_v<const U&, T>);

    template <class U>
    constexpr explicit(!std::is_convertible_v<U, T>) optional(optional<U>&& rhs)
        requires(!std::is_reference_v<U> && detail::enable_from_other<T, U, U &&> && std::is_convertible_v<U &&, T>);
    template <class U>
    constexpr explicit(!std::is_convertible_v<U, T>) optional(optional<U>&& rhs)
        requires(!std::is_reference_v<U> && detail::enable_from_other<T, U, U &&> && !std::is_convertible_v<U &&, T>);

    template <class U>
    constexpr explicit(!std::is_convertible_v<U, T>) optional(const optional<U&>& rhs)
        requires(detail::enable_from_other<T, U, U&> && std::is_convertible_v<U&, T>);
    template <class U>
    constexpr explicit(!std::is_convertible_v<U, T>) optional(const optional<U&>& rhs)
        requires(detail::enable_from_other<T, U, U&> && !std::is_convertible_v<U&, T>);

...this adds !std::is_reference_v<U> constraints to the optional<U> overloads, and the constraints for optional<U&> check for constructibility from U& (without const, since const is shallow).

@steve-downey
Copy link
Member

steve-downey commented Jan 2, 2025 via email

@jiixyj
Copy link
Contributor Author

jiixyj commented Jan 3, 2025

I've implemented the approach with the separated overloads for optional<U&>. And added some more exhaustive tests to really make sure the desired operations on the U& are called. It is now nicely symmetrical compared to the monadic operations that also always operate on the U& regardless of the value category of the optional<U&>.

Or using an if constexpr to change the body of the affected assignment operators

Can you elaborate? I'm not sure how this would look like, as the constraints are different for all the overloads, especially the detail::enable_from_other. For example it would be bad if the detail::enable_from_other checked constructibility from const U&, but then inside the body one would construct from U&.

@steve-downey
Copy link
Member

steve-downey commented Jan 3, 2025 via email

@steve-downey
Copy link
Member

steve-downey commented Jan 4, 2025

A simple set of failing test cases

TEST(OptionalTest, TestStealingFromReference) {
    beman::optional26::tests::copyable_move_detector                               m;
    beman::optional26::optional<beman::optional26::tests::copyable_move_detector&> om{m};
    beman::optional26::optional<beman::optional26::tests::copyable_move_detector>  o{std::move(om)};
    EXPECT_FALSE(m.been_moved);
}

TEST(OptionalTest, TestAssignStealingFromReference) {
    beman::optional26::tests::copyable_move_detector                               m;
    beman::optional26::optional<beman::optional26::tests::copyable_move_detector&> om{m};
    beman::optional26::optional<beman::optional26::tests::copyable_move_detector>  o;
    o = std::move(om);
    EXPECT_FALSE(m.been_moved);
}

where copyable_move_detector is

struct copyable_move_detector {
    constexpr copyable_move_detector()                            = default;
    constexpr copyable_move_detector(const copyable_move_detector&) = default;
    constexpr copyable_move_detector(copyable_move_detector&& rhs) { rhs.been_moved = true; }
    constexpr copyable_move_detector& operator=(const copyable_move_detector&) = default;
    constexpr copyable_move_detector& operator=(copyable_move_detector&& rhs) { rhs.been_moved = true; return *this;}

    bool been_moved = false;
};

(not modifying move_detector for fear of affecting something depending on it not being copyable at the moment.)

That is - these tests fail on main and pass on this branch.

@steve-downey
Copy link
Member

Can you merge main back into this branch to resolve conflicts? Otherwise I'll do so from the command line and push.

@jiixyj
Copy link
Contributor Author

jiixyj commented Jan 6, 2025

Can you merge main back into this branch to resolve conflicts? Otherwise I'll do so from the command line and push.

Sure thing, I've just merged the main branch back.

@steve-downey steve-downey merged commit bb60b34 into bemanproject:main Jan 6, 2025
9 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.

What should moving from optional<T&> into optional<T> do?
4 participants