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

[libc++] restrict the expected conversion constructor not compete against copy constructor #96101

Merged
merged 4 commits into from
Jun 26, 2024

Conversation

huixie90
Copy link
Contributor

@huixie90 huixie90 commented Jun 19, 2024

fixes #92676

So right now clang does not like

std::expected<std::any, int> e1;
auto e2 = e1;

So basically when clang tries to do overload resolution of auto e2 = e1;

It finds

expected(const expected&);  // 1. This is OK
expected(const expected<_Up, _OtherErr>&)  requires __can_convert; // 2. This needs to check its constraints

Then in __can_convert, one of the check is

_Not<is_constructible<_Tp, expected<_Up, _OtherErr>&>>

which is checking

is_constructible<std::any, expected<_Up, _OtherErr>&>

Then it looks at std::any's constructor

  template < class _ValueType,
             class _Tp = decay_t<_ValueType>,
             class     = enable_if_t< !is_same<_Tp, any>::value && !__is_inplace_type<_ValueType>::value &&
                                      is_copy_constructible<_Tp>::value> >
  any(_ValueType&& __value);

In the above, is_copy_constructible<_Tp> expands to

is_copy_constructible<std::expected<std::any, int>>

And the above goes back to the original thing we asked : copy the std::expected, which goes to the overload resolution again.

expected(const expected&);
expected(const expected<_Up, _OtherErr>&)  requires __can_convert;

So the second overload results in a logical cycle.

I am not a language lawyer. We could argue that clang should give up on the second overload which has logical cycle, as the first overload is a perfect match.

Anyway, the fix in this patch tries to short-circuiting the second overload's constraint check: that is, if the argument matches exact same expected<T, E>, we give up immediately and let the copy constructor to deal with it

@huixie90 huixie90 requested a review from a team as a code owner June 19, 2024 18:50
@llvmbot llvmbot added the libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi. label Jun 19, 2024
@llvmbot
Copy link
Collaborator

llvmbot commented Jun 19, 2024

@llvm/pr-subscribers-libcxx

Author: Hui (huixie90)

Changes

fixes #92676


Full diff: https://github.com/llvm/llvm-project/pull/96101.diff

2 Files Affected:

  • (modified) libcxx/include/__expected/expected.h (+3-1)
  • (modified) libcxx/test/std/utilities/expected/expected.expected/ctor/ctor.copy.pass.cpp (+18)
diff --git a/libcxx/include/__expected/expected.h b/libcxx/include/__expected/expected.h
index 0f994e297a877..da0a0fa85714c 100644
--- a/libcxx/include/__expected/expected.h
+++ b/libcxx/include/__expected/expected.h
@@ -507,7 +507,9 @@ class expected : private __expected_base<_Tp, _Err> {
       _And< is_constructible<_Tp, _UfQual>,
             is_constructible<_Err, _OtherErrQual>,
             _If<_Not<is_same<remove_cv_t<_Tp>, bool>>::value,
-                _And< _Not<is_constructible<_Tp, expected<_Up, _OtherErr>&>>,
+                _And< 
+                      _Not<_And<is_same<_Tp, _Up>, is_same<_Err, _OtherErr>>>,
+                      _Not<is_constructible<_Tp, expected<_Up, _OtherErr>&>>,
                       _Not<is_constructible<_Tp, expected<_Up, _OtherErr>>>,
                       _Not<is_constructible<_Tp, const expected<_Up, _OtherErr>&>>,
                       _Not<is_constructible<_Tp, const expected<_Up, _OtherErr>>>,
diff --git a/libcxx/test/std/utilities/expected/expected.expected/ctor/ctor.copy.pass.cpp b/libcxx/test/std/utilities/expected/expected.expected/ctor/ctor.copy.pass.cpp
index 581df51207da2..6c278be9de98a 100644
--- a/libcxx/test/std/utilities/expected/expected.expected/ctor/ctor.copy.pass.cpp
+++ b/libcxx/test/std/utilities/expected/expected.expected/ctor/ctor.copy.pass.cpp
@@ -62,6 +62,17 @@ static_assert(!std::is_trivially_copy_constructible_v<std::expected<CopyableNonT
 static_assert(!std::is_trivially_copy_constructible_v<std::expected<int, CopyableNonTrivial>>);
 static_assert(!std::is_trivially_copy_constructible_v<std::expected<CopyableNonTrivial, CopyableNonTrivial>>);
 
+
+struct Any {
+  constexpr Any() = default;
+  constexpr Any(const Any&) = default;
+  constexpr Any& operator=(const Any&)=default;
+
+  template <class T>
+    requires (!std::is_same_v<Any, std::decay_t<T>> && std::is_copy_constructible_v<std::decay_t<T>>)
+  constexpr Any(T&&){}
+};
+
 constexpr bool test() {
   // copy the value non-trivial
   {
@@ -109,6 +120,13 @@ constexpr bool test() {
     assert(!e2.has_value());
   }
 
+  {
+    // https://github.com/llvm/llvm-project/issues/92676
+    std::expected<Any, int> e1;
+    auto e2 = e1;
+    assert(e2.has_value());
+  }
+
   return true;
 }
 

Copy link
Member

@ldionne ldionne left a comment

Choose a reason for hiding this comment

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

This LGTM with green CI and comments applied.

Also, please file a bug report against Clang, it seems like this could be a compiler issue: https://godbolt.org/z/83Yfsvbr5

If this isn't a compiler issue, then this will likely lead to a LWG issue because this code should definitely compile.

libcxx/include/__expected/expected.h Outdated Show resolved Hide resolved
@huixie90 huixie90 merged commit 16f3492 into llvm:main Jun 26, 2024
53 of 54 checks passed
lravenclaw pushed a commit to lravenclaw/llvm-project that referenced this pull request Jul 3, 2024
…inst copy constructor (llvm#96101)

fixes llvm#92676

So right now clang does not like 
```
std::expected<std::any, int> e1;
auto e2 = e1;

```

So basically when clang tries to do overload resolution of `auto e2 =
e1;`

It finds

```
expected(const expected&);  // 1. This is OK
expected(const expected<_Up, _OtherErr>&)  requires __can_convert; // 2. This needs to check its constraints
```

Then in `__can_convert`, one of the check is 

```
_Not<is_constructible<_Tp, expected<_Up, _OtherErr>&>>
```
which is checking 
```
is_constructible<std::any, expected<_Up, _OtherErr>&>
```

Then it looks at `std::any`'s constructor
```
  template < class _ValueType,
             class _Tp = decay_t<_ValueType>,
             class     = enable_if_t< !is_same<_Tp, any>::value && !__is_inplace_type<_ValueType>::value &&
                                      is_copy_constructible<_Tp>::value> >
  any(_ValueType&& __value);
```
In the above, `is_copy_constructible<_Tp>` expands to
```
is_copy_constructible<std::expected<std::any, int>>
```

And the above goes back to the original thing we asked : copy the
`std::expected`, which goes to the overload resolution again.

```
expected(const expected&);
expected(const expected<_Up, _OtherErr>&)  requires __can_convert;
```

So the second overload results in a logical cycle. 

I am not a language lawyer. We could argue that clang should give up on
the second overload which has logical cycle, as the first overload is a
perfect match.

Anyway, the fix in this patch tries to short-circuiting the second
overload's constraint check: that is, if the argument matches exact same
`expected<T, E>`, we give up immediately and let the copy constructor to
deal with it
AlexisPerry pushed a commit to llvm-project-tlp/llvm-project that referenced this pull request Jul 9, 2024
…inst copy constructor (llvm#96101)

fixes llvm#92676

So right now clang does not like 
```
std::expected<std::any, int> e1;
auto e2 = e1;

```

So basically when clang tries to do overload resolution of `auto e2 =
e1;`

It finds

```
expected(const expected&);  // 1. This is OK
expected(const expected<_Up, _OtherErr>&)  requires __can_convert; // 2. This needs to check its constraints
```

Then in `__can_convert`, one of the check is 

```
_Not<is_constructible<_Tp, expected<_Up, _OtherErr>&>>
```
which is checking 
```
is_constructible<std::any, expected<_Up, _OtherErr>&>
```

Then it looks at `std::any`'s constructor
```
  template < class _ValueType,
             class _Tp = decay_t<_ValueType>,
             class     = enable_if_t< !is_same<_Tp, any>::value && !__is_inplace_type<_ValueType>::value &&
                                      is_copy_constructible<_Tp>::value> >
  any(_ValueType&& __value);
```
In the above, `is_copy_constructible<_Tp>` expands to
```
is_copy_constructible<std::expected<std::any, int>>
```

And the above goes back to the original thing we asked : copy the
`std::expected`, which goes to the overload resolution again.

```
expected(const expected&);
expected(const expected<_Up, _OtherErr>&)  requires __can_convert;
```

So the second overload results in a logical cycle. 

I am not a language lawyer. We could argue that clang should give up on
the second overload which has logical cycle, as the first overload is a
perfect match.

Anyway, the fix in this patch tries to short-circuiting the second
overload's constraint check: that is, if the argument matches exact same
`expected<T, E>`, we give up immediately and let the copy constructor to
deal with it
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
libc++ libc++ C++ Standard Library. Not GNU libstdc++. Not libc++abi.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[libc++] std::expected does not work properly with std::any
3 participants