Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 39 additions & 3 deletions stl/inc/flat_set
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@ public:

_Base_flat_set() : _Mycont(), _Mycomp() {}

// TRANSITION, "_Mycomp" may need to be copied, even in move construction / assignment.
template <_Allocator_for<container_type> _Alloc>
_Base_flat_set(const _Deriv& _Set, const _Alloc& _Al) : _Mycont(_Set._Mycont, _Al), _Mycomp(_Set._Mycomp) {}
template <_Allocator_for<container_type> _Alloc>
_Base_flat_set(_Deriv&& _Set, const _Alloc& _Al)
: _Mycont(_STD move(_Set._Mycont), _Al), _Mycomp(_STD move(_Set._Mycomp)) {}
: _Mycont(_STD move(_Set).extract(), _Al), _Mycomp(_Set._Mycomp) // intentionally copy comparator, see LWG-2227

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Unlike _Base_flat_set(_Base_flat_set&& _Other), this extract() might cause unwanted materialization...

{}

explicit _Base_flat_set(container_type _Cont, const key_compare& _Comp = key_compare())
: _Mycont(_STD move(_Cont)), _Mycomp(_Comp) {
Expand Down Expand Up @@ -153,6 +153,32 @@ public:
_Base_flat_set(_Tsorted _Tsort, initializer_list<_Kty> _Ilist, const _Alloc& _Al)
: _Base_flat_set(_Tsort, container_type(_Ilist, _Al)) {}

_Base_flat_set(const _Base_flat_set&) = default;
_Base_flat_set(_Base_flat_set&& _Other) noexcept(
is_nothrow_move_constructible_v<container_type>&& is_nothrow_copy_constructible_v<key_compare>) // strengthened
: _Mycont(_STD move(_Other).extract()), _Mycomp(_Other._Mycomp) // intentionally copy comparator, see LWG-2227
Comment thread
achabense marked this conversation as resolved.
{}

_Base_flat_set& operator=(const _Base_flat_set& _Other) {
_Clear_guard<_Base_flat_set> _Guard{this};

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

_Guard is needed when:

  1. the copy assignment fails and cannot provide strong-guarantee, or
  2. _Mycomp is different and throws during copy.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Though not necessary, I think it looks strange that move-assignment has address checking, while this doesn't have :|

_Mycont = _Other._Mycont;
_Mycomp = _Other._Mycomp;
_Guard._Target = nullptr;
return *this;
}
_Base_flat_set& operator=(_Base_flat_set&& _Other) noexcept(
is_nothrow_move_assignable_v<container_type>&& is_nothrow_copy_assignable_v<key_compare>) // strengthened

@achabense achabense Oct 11, 2023

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This PR is currently making move functions unconditionally noexcept(false), which looks more buggy to me.

Yes, added. (I'm adding // strengthened; are these conditional noexcept mandated by the standard?)

It seems that we can wrap the comparator into the following class to enforce copy on moving and leave the copy/move confuntions defaulted.

I think there is no need to introduce a copy wrapper, as they are solely for defaulting the functions, but the defaulted functions are not capable to deal with a lot of invariant-breaking cases.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

are these conditional noexcept mandated by the standard?

I think sometimes exposition only non-static data members are considered completely specifying the behavior of the implicitly declared or explicitly defaulted special member functions, e.g. for many ranges types and flat container adaptors, but definitely not for optional.

However, it's unfortunately underspecified whether non-static data members are considered complete. It seems permitted for implementations to add "evil" subobjects that weaken defaulted exception specifications.

Per [functions.within.classes], I think the standard currently implicitly requires that the copy/move operations are effectively defaulted and are able to break invariants.

We should report an LWG issue that makes copy/move functions explicitly specified. I believe we should say // per an unnumbered LWG issue instead of // strengthened for now.

{
if (this != _STD addressof(_Other)) {
_Clear_guard<_Base_flat_set> _Guard{this};
_Clear_guard<_Base_flat_set> _Always_clear{_STD addressof(_Other)};
_Mycont = _STD move(_Other._Mycont);
_Mycomp = _Other._Mycomp; // intentionally copy comparator, see LWG-2227
_Guard._Target = nullptr;
}
return *this;
}

_Deriv& operator=(initializer_list<_Kty> _Ilist) {
_Clear_guard<_Base_flat_set> _Guard{this};
_Mycont.assign(_Ilist);
Expand Down Expand Up @@ -288,7 +314,7 @@ public:
_NODISCARD container_type extract() && noexcept(
is_nothrow_move_constructible_v<container_type>) /* strengthened */ {
// always clears the container (N4950 [flat.set.modifiers]/14 and [flat.multiset.modifiers]/10)
_Clear_guard<_Base_flat_set> _Guard{this};
_Clear_guard<_Base_flat_set> _Always_clear{this};
return _STD move(_Mycont);
}
void replace(container_type&& _Cont) {
Expand Down Expand Up @@ -711,7 +737,12 @@ private:

public:
using _Mybase::_Mybase;
flat_set(const flat_set&) = default;
flat_set(flat_set&&) = default;

using _Mybase::operator=;
flat_set& operator=(const flat_set&) = default;
flat_set& operator=(flat_set&&) = default;
};

_EXPORT_STD template <class _Kty, class _Keylt = less<_Kty>, class _Container = vector<_Kty>>
Expand All @@ -722,7 +753,12 @@ private:

public:
using _Mybase::_Mybase;
flat_multiset(const flat_multiset&) = default;
flat_multiset(flat_multiset&&) = default;

using _Mybase::operator=;
flat_multiset& operator=(const flat_multiset&) = default;
flat_multiset& operator=(flat_multiset&&) = default;
};

_EXPORT_STD template <class _Kty, class _Keylt, class _Container, class _Pred>
Expand Down
158 changes: 157 additions & 1 deletion tests/std/tests/P1222R4_flat_set/test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include <algorithm>
#include <cassert>
#include <climits>
#include <deque>
#include <flat_set>
#include <functional>
Expand Down Expand Up @@ -69,10 +70,14 @@ void assert_reversible_container_requirements(const T& s) {
}

template <class T>
void assert_all_requirements_and_equals(const T& s, const initializer_list<typename T::value_type>& il) {
void assert_all_requirements(const T& s) {
assert_container_requirements(s);
assert_reversible_container_requirements(s);

// FIXME, in GH-4084
// assert_noexcept_requirements(s);
// assert_noexcept_requirements(const_cast<T&>(s));

auto val_comp = s.value_comp();
auto begin_it = s.cbegin();
auto end_it = s.cend();
Expand All @@ -85,6 +90,11 @@ void assert_all_requirements_and_equals(const T& s, const initializer_list<typen
}
}
}
}

template <class T>
void assert_all_requirements_and_equals(const T& s, const initializer_list<typename T::value_type>& il) {
assert_all_requirements(s);

if (!std::equal(s.begin(), s.end(), il.begin(), il.end())) {
cout << "Expected: {";
Expand Down Expand Up @@ -467,6 +477,151 @@ void test_extract_2() {
assert_all_requirements_and_equals(fs, {}); // assert empty
}

void test_invariant_robustness() {
static int copy_limit = 2;
constexpr int unlimited = INT_MAX;

struct odd_key {
static void countdown() {
if (copy_limit == unlimited) {
return;
}

if (--copy_limit < 0) {
throw 0; // will be caught by "catch (...)".
}
}

int key;

odd_key(int k = 0) : key(k) {}

bool operator==(const odd_key&) const = default;

odd_key(const odd_key& other) {
countdown();
key = other.key;
}

odd_key(odd_key&& other) {
countdown();
key = exchange(other.key, 0);
}

odd_key& operator=(const odd_key& other) {
countdown();
key = other.key;
return *this;
}

odd_key& operator=(odd_key&& other) {
countdown();
key = exchange(other.key, 0);
return *this;
}
};

class odd_container : public vector<odd_key> {
private:
using base = vector<odd_key>;

public:
using base::base;
odd_container(const odd_container&) = default;

// this copy-assignment cannot provide strong-guarantee for `this`:
odd_container& operator=(const odd_container& other) {
resize(other.size());
std::copy(other.begin(), other.end(), begin());
return *this;
}

// this move-ctor cannot provide strong-guarantee for `other`, and even successful, will leave elements of
// `other` in moved-from state:
odd_container(odd_container&& other) {
reserve(other.size());
for (auto& e : other) {
push_back(std::move(e));
}
}

// this move-assignment cannot provide strong-guarantee for `this` and `other`, and even successful, will leave
// elements of `other` in moved-from state:
odd_container& operator=(odd_container&& other) {
resize(other.size());
std::move(other.begin(), other.end(), begin());
return *this;
}
};

using SetT = flat_set<odd_key, key_comparer, odd_container>;

// copy-assignment
{
copy_limit = unlimited;
SetT fs1{0, 1, 2, 3, 4};
SetT fs2{5, 6, 7, 8, 9};

assert(ranges::equal(fs1, vector{0, 1, 2, 3, 4}, {}, &odd_key::key));
assert(ranges::equal(fs2, vector{5, 6, 7, 8, 9}, {}, &odd_key::key));

bool caught = false;
try {
copy_limit = 2;
fs1 = fs2; // will throw after copying 2 odd_key.
} catch (...) {
copy_limit = unlimited;
assert_all_requirements(fs1);
caught = true;
}
assert(caught);
}
// move-ctor
{
copy_limit = unlimited;
SetT fs1{0, 1, 2, 3, 4};
SetT fs2{std::move(fs1)};

assert_all_requirements(fs1);
assert(ranges::equal(fs2, vector{0, 1, 2, 3, 4}, {}, &odd_key::key));

bool caught = false;
try {
copy_limit = 2;
SetT fs3{std::move(fs2)}; // will throw after moving 2 odd_key.
} catch (...) {
copy_limit = unlimited;
assert_all_requirements(fs2);
caught = true;
}
assert(caught);
}
// move-assignment
{
copy_limit = unlimited;
SetT fs1{0, 1, 2, 3, 4};
SetT fs2;
SetT fs3{5, 6, 7, 8, 9};
fs2 = std::move(fs1);

assert_all_requirements(fs1);
assert(ranges::equal(fs2, vector{0, 1, 2, 3, 4}, {}, &odd_key::key));
assert(ranges::equal(fs3, vector{5, 6, 7, 8, 9}, {}, &odd_key::key));

bool caught = false;
try {
copy_limit = 2;
fs2 = std::move(fs3); // will throw after moving 2 odd_key.
} catch (...) {
copy_limit = unlimited;
assert_all_requirements(fs2);
assert_all_requirements(fs3);
caught = true;
}
assert(caught);
}
}

// TRANSITION, too simple
void test_erase_1() {
flat_set<int> fs{1};
Expand Down Expand Up @@ -544,6 +699,7 @@ int main() {

test_erase_1();
test_erase_2();
test_invariant_robustness();

test_erase_if<flat_set<int>>();
test_erase_if<flat_multiset<int>>();
Expand Down