| Issue |
109996
|
| Summary |
[libc++] `std::equal` possibly redundant unwrap of iterators
|
| Labels |
libc++
|
| Assignees |
|
| Reporter |
akukh
|
Attempting to call `std::equal` on two ranges of elements for comparison fails in constant _expression_. See the following example (sorry in advance for a lot of code):
<details>
<summary>Code</summary>
```c++
#include <utility>
#include <algorithm>
#include <array>
#include <vector>
struct nontrivial_type {
constexpr nontrivial_type() noexcept {}
};
template <typename T>
union [[nodiscard]] uninitialized_storage final {
using value_type = T;
value_type value;
nontrivial_type _;
constexpr uninitialized_storage() : _{} {}
constexpr uninitialized_storage(uninitialized_storage const& other) noexcept
: value{other.value} {}
constexpr uninitialized_storage(uninitialized_storage&& other) noexcept
: value{std::move(other.value)} {}
constexpr uninitialized_storage& operator=(uninitialized_storage const&) noexcept = delete;
constexpr uninitialized_storage& operator=(uninitialized_storage&&) noexcept = delete;
constexpr ~uninitialized_storage() {}
};
struct nontrivial_int {
int x;
constexpr nontrivial_int() noexcept : x{0} {}
constexpr nontrivial_int(int val) noexcept : x{val} {}
constexpr ~nontrivial_int() {}
constexpr nontrivial_int(nontrivial_int const& other) noexcept {
x = other.x;
}
constexpr nontrivial_int(nontrivial_int&& other) noexcept {
x = other.x;
}
constexpr nontrivial_int& operator=(nontrivial_int const& other) noexcept {
x = other.x;
return *this;
}
constexpr nontrivial_int& operator=(nontrivial_int&& other) noexcept {
x = other.x;
return *this;
}
constexpr bool operator==(nontrivial_int const& other) const noexcept {
return x == other.x;
}
};
template <typename It, typename Fn>
class iterator_adapter final {
using functor_type = Fn;
public:
using reference = decltype(::std::declval<Fn>()(*::std::declval<It>()));
using value_type = ::std::remove_reference_t<reference>;
using pointer = ::std::add_pointer_t<value_type>;
using difference_type = ::std::iter_difference_t<It>;
using iterator_type = It;
using iterator_category = ::std::random_access_iterator_tag;
using iterator_concept = ::std::contiguous_iterator_tag;
iterator_type it_;
functor_type functor_;
constexpr iterator_adapter() noexcept : it_{}, functor_{} {}
constexpr iterator_adapter(iterator_type const& it, functor_type adapter = {}) noexcept
: it_{it}, functor_{adapter} {}
[[nodiscard]] constexpr reference operator*() const noexcept {
return *operator->();
}
[[nodiscard]] constexpr pointer operator->() const noexcept {
return ::std::addressof(functor_(*it_));
}
[[nodiscard]] constexpr reference operator[](difference_type index) const noexcept {
return functor_(it_[index]);
}
constexpr iterator_adapter& operator++() noexcept {
++it_;
return *this;
}
constexpr iterator_adapter operator++(int) noexcept {
return {it_++, functor_};
}
constexpr iterator_adapter& operator--() noexcept {
--it_;
return *this;
}
constexpr iterator_adapter operator--(int) noexcept {
return {it_--, functor_};
}
constexpr iterator_adapter& operator+=(difference_type n) noexcept {
it_ += n;
return *this;
}
constexpr iterator_adapter& operator-=(difference_type n) noexcept {
it_ -= n;
return *this;
}
constexpr iterator_adapter operator+(difference_type n) const noexcept {
return {it_ + n, functor_};
}
constexpr iterator_adapter operator-(difference_type n) const noexcept {
return {it_ - n, functor_};
}
[[nodiscard]] constexpr difference_type
operator-(iterator_adapter const& other) const noexcept {
return it_ - other.it_;
}
[[nodiscard]] constexpr std::strong_ordering
operator<=>(iterator_adapter const& other) const noexcept {
return it_ <=> other.it_;
}
[[nodiscard]] constexpr bool operator==(iterator_adapter const& other) const noexcept {
return it_ == other.it_;
}
friend constexpr auto operator+(difference_type n, iterator_adapter const& a) noexcept {
return iterator_adapter{a.it_ + n, a.functor_};
}
};
template <typename T, ::std::size_t N>
struct static_vector final {
struct [[nodiscard]] mapper final {
static constexpr T& operator()(uninitialized_storage<T>& storage) noexcept {
return storage.value;
}
static constexpr T const& operator()(uninitialized_storage<T> const& storage) noexcept {
return storage.value;
}
};
using iterator = iterator_adapter<
typename ::std::array<uninitialized_storage<T>, N>::iterator,
mapper
>;
using const_iterator = iterator_adapter<
typename ::std::array<uninitialized_storage<T>, N>::const_iterator,
mapper
>;
::std::array<uninitialized_storage<T>, N> storage_;
::std::size_t size_;
constexpr static_vector() noexcept : size_{} {
if consteval {
::std::construct_at(::std::addressof(storage_));
}
}
constexpr iterator begin() noexcept {
return {storage_.begin()};
}
constexpr const_iterator begin() const noexcept {
return {storage_.begin()};
}
constexpr iterator end() noexcept {
return {storage_.begin() + size_};
}
constexpr const_iterator end() const noexcept {
return {storage_.begin() + size_};
}
template <typename... Args>
constexpr T& emplace_back(Args&&... args) noexcept {
::std::construct_at(end().operator->(), ::std::forward<Args>(args)...);
++size_;
return storage_[size_ - 1].value;
}
template <::std::size_t OtherCapacity>
constexpr bool operator==(static_vector<T, OtherCapacity> const& other) const noexcept {
return ::std::equal(begin(), end(), other.begin());
}
};
int main() {
constexpr auto result = [] {
static_vector<nontrivial_int, 3> v1;
v1.emplace_back(1);
v1.emplace_back(2);
static_vector<nontrivial_int, 3> v2;
v2.emplace_back(1);
v2.emplace_back(2);
return v1 == v2;
}();
static_assert(true == result);
}
```
</details>
Fails with the following errors:
<details>
<summary>Output</summary>
```
<source>:203:20: error: constexpr variable 'result' must be initialized by a constant _expression_
203 | constexpr auto result = [] {
| ^ ~~~~
204 | static_vector<nontrivial_int, 3> v1;
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
205 | v1.emplace_back(1);
| ~~~~~~~~~~~~~~~~~~~
206 | v1.emplace_back(2);
| ~~~~~~~~~~~~~~~~~~~
207 |
208 | static_vector<nontrivial_int, 3> v2;
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
209 | v2.emplace_back(1);
| ~~~~~~~~~~~~~~~~~~~
210 | v2.emplace_back(2);
| ~~~~~~~~~~~~~~~~~~~
211 |
212 | return v1 == v2;
| ~~~~~~~~~~~~~~~~
213 | }();
| ~~~
/opt/compiler-explorer/clang-18.1.0/bin/../include/c++/v1/__algorithm/comp.h:25:16: note: member call on dereferenced one-past-the-end pointer is not allowed in a constant _expression_
25 | return __x == __y;
| ^
/opt/compiler-explorer/clang-18.1.0/bin/../include/c++/v1/__algorithm/equal.h:42:10: note: in call to '__pred.operator()<nontrivial_int, nontrivial_int>(*(&v1.storage_.__elems_[0].value + 1), *(&v2.storage_.__elems_[0].value + 1))'
42 | if (!__pred(*__first1, *__first2))
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~
/opt/compiler-explorer/clang-18.1.0/bin/../include/c++/v1/__algorithm/equal.h:61:10: note: in call to '__equal_iter_impl<const nontrivial_int *, const nontrivial_int *, std::__equal_to>(&v1.storage_.__elems_[0].value + 1, &v1.storage_.__elems_[2].value, &v2.storage_.__elems_[0].value + 1, __pred)'
61 | return std::__equal_iter_impl(
| ^~~~~~~~~~~~~~~~~~~~~~~
62 | std::__unwrap_iter(__first1), std::__unwrap_iter(__last1), std::__unwrap_iter(__first2), __pred);
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/opt/compiler-explorer/clang-18.1.0/bin/../include/c++/v1/__algorithm/equal.h:68:10: note: in call to 'equal<iterator_adapter<const uninitialized_storage<nontrivial_int> *, static_vector<nontrivial_int, 3>::mapper>, iterator_adapter<const uninitialized_storage<nontrivial_int> *, static_vector<nontrivial_int, 3>::mapper>, std::__equal_to>({&v1.storage_.__elems_[0], {}}, {&v1.storage_.__elems_[2], {}}, {&v2.storage_.__elems_[0], {}}, {})'
68 | return std::equal(__first1, __last1, __first2, __equal_to());
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:198:16: note: in call to 'equal<iterator_adapter<const uninitialized_storage<nontrivial_int> *, static_vector<nontrivial_int, 3>::mapper>, iterator_adapter<const uninitialized_storage<nontrivial_int> *, static_vector<nontrivial_int, 3>::mapper>>({&v1.storage_.__elems_[0], {}}, {&v1.storage_.__elems_[2], {}}, {&v2.storage_.__elems_[0], {}})'
198 | return ::std::equal(begin(), end(), other.begin());
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
<source>:212:16: note: in call to 'v1.operator==<3UL>(v2)'
212 | return v1 == v2;
| ^~~~~~~~
<source>:203:29: note: in call to '[] {
static_vector<nontrivial_int, 3> v1;
v1.emplace_back(1);
v1.emplace_back(2);
static_vector<nontrivial_int, 3> v2;
v2.emplace_back(1);
v2.emplace_back(2);
return v1 == v2;
}.operator()()'
203 | constexpr auto result = [] {
| ^~~~
204 | static_vector<nontrivial_int, 3> v1;
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
205 | v1.emplace_back(1);
| ~~~~~~~~~~~~~~~~~~~
206 | v1.emplace_back(2);
| ~~~~~~~~~~~~~~~~~~~
207 |
208 | static_vector<nontrivial_int, 3> v2;
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
209 | v2.emplace_back(1);
| ~~~~~~~~~~~~~~~~~~~
210 | v2.emplace_back(2);
| ~~~~~~~~~~~~~~~~~~~
211 |
212 | return v1 == v2;
| ~~~~~~~~~~~~~~~~
213 | }();
| ~~~
<source>:214:19: error: static assertion _expression_ is not an integral constant _expression_
214 | static_assert(true == result);
| ^~~~~~~~~~~~~~
<source>:214:27: note: initializer of 'result' is not a constant _expression_
214 | static_assert(true == result);
| ^
<source>:203:20: note: declared here
203 | constexpr auto result = [] {
| ^
2 errors generated.
```
</details>
Compiler options:
```
-std=c++23 -stdlib=libc++
```
[godbolt](https://godbolt.org/z/xETjjdMhf)
GCC with their `libstdc++` compiles and runs successfully. In this question, is the problem with `libc++` or does `libstdc++` incorrectly implement `std::equal`?
_______________________________________________
llvm-bugs mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-bugs