@@ -18,61 +18,182 @@ struct NODISCARD TinySet
1818{
1919private:
2020 using Type = Type_;
21- using Set = Set_;
22- using PtrToSet = std::unique_ptr<Set>;
23-
24- // REVISIT: if the type can spare a bit, then they can be equal.
25- /*
26- static_assert(sizeof(Type) < sizeof(uintptr_t));
27- static_assert(sizeof(uintptr_t) >= sizeof(PtrToSet));
28- */
21+ using BigSet = Set_; // big as opposed to tiny
22+ using UniqueBigSet = std::unique_ptr<BigSet>;
2923
3024private:
31- uintptr_t m_storage = 0 ;
25+ // This Variant class is basically just a "compiler approved" union of Type and UniqueBig,
26+ // where the least significant bit decides which type it is.
27+ //
28+ // Note that Storage does not control the lifetime of the pointed-to-set,
29+ // but TinySet does control the lifetime of that object.
30+ struct NODISCARD Variant final
31+ {
32+ private:
33+ static inline constexpr size_t BUF_SIZE = sizeof (uintptr_t );
34+ static inline constexpr size_t BUF_ALIGN = alignof (uintptr_t );
35+ static_assert (BUF_SIZE == sizeof (UniqueBigSet));
36+ static_assert (BUF_SIZE >= sizeof (size_t ));
37+ static_assert (sizeof (size_t ) >= sizeof (Type)); /* note: must be at least one bit larger */
38+ alignas (BUF_ALIGN ) char m_buf[BUF_SIZE ]{0 };
39+
40+ public:
41+ NODISCARD bool operator ==(const Variant &other) const noexcept
42+ {
43+ return std::memcmp (m_buf, other.m_buf , BUF_SIZE ) == 0 ;
44+ }
45+
46+ private:
47+ // holds_xxx() functions are based on this.
48+ //
49+ // Note: The copy is made to avoid the following dreaded sequence of events:
50+ // 1) auto *ptr = std::launder(reinterpret_cast<UniqueBigSet*>(buf));
51+ // 2) auto tmp = *std::launder(reinterpret_cast<uintptr_t*>(buf)); // invalidates ptr
52+ // 3) use(ptr); // UB
53+ //
54+ // The reason this is UB is because of strict pointer aliasing rule that essentially
55+ // allows the compiler to assume there can only be only one laundered "object"
56+ // (or primitive type) at a given address, with the notable exception that it's also
57+ // legal to have a pointer to a type that provides storage (e.g. char).
58+ //
59+ // So this means if the compiler can detect the start of the lifetime of a new object
60+ // at the same address, then the old object's lifetime has ended. This means we cannot
61+ // simultaneously maintain a pointer in the storage -and- also examine it as a uintptr_t.
62+ // (Hopefully this rule will be relaxed for primitive types in a future c++ standard.)
63+ //
64+ // Another option would be to detect if the platform is big or little endian,
65+ // and then just read the buffer's high or low byte to know the type.
66+ // (I'm too lazy to check the assembly to see if either option is actually better,
67+ // but I assume that the compiler will figure out both, so I'd expect both
68+ // to yield the same assembly code.)
69+ NODISCARD uintptr_t get_uintptrt () const noexcept
70+ {
71+ const Variant copy = *this ; // compiler performs a memcpy
72+ return *std::launder (reinterpret_cast <const uintptr_t *>(copy.m_buf ));
73+ }
74+
75+ public:
76+ // empty is effectively just holding "(UniqueBig*)nullptr"
77+ NODISCARD bool holds_nothing () const noexcept { return get_uintptrt () == 0u ; }
78+ NODISCARD bool holds_single_value () const noexcept { return (get_uintptrt () & 1u ) == 1u ; }
79+ NODISCARD bool holds_unique_bigset () const noexcept
80+ {
81+ return !holds_nothing () && !holds_single_value ();
82+ }
83+
84+ public:
85+ void clear () noexcept
86+ {
87+ if (holds_unique_bigset ()) {
88+ assign_unique_bigset (nullptr );
89+ } else {
90+ // would it be okay to write "*this = {};" here?
91+ *std::launder (reinterpret_cast <uintptr_t *>(m_buf)) = 0u ;
92+ }
93+ assert (holds_nothing ());
94+ }
95+
96+ public:
97+ NODISCARD Type get_single_value () const noexcept
98+ {
99+ assert (holds_single_value ());
100+ const auto ui = get_uintptrt () >> 1u ;
101+ const auto size = static_cast <size_t >(ui);
102+ return IdHelper_::fromBits (size);
103+ }
104+ void set_single_value (const Type value) noexcept
105+ {
106+ if (holds_unique_bigset ()) {
107+ assert (!holds_unique_bigset ());
108+ std::abort (); // crash
109+ }
110+
111+ constexpr size_t MASK = ~size_t {0 } >> 1u ;
112+ const auto raw_val = static_cast <size_t >(IdHelper_::getBits (value));
113+ const auto masked_val = static_cast <uintptr_t >(raw_val & MASK );
114+ assert (raw_val == masked_val);
115+ const auto ui = (masked_val << 1u ) | 1u ;
116+ assert (holds_nothing () || holds_single_value ());
117+ *std::launder (reinterpret_cast <uintptr_t *>(m_buf)) = ui;
118+ assert (holds_single_value ());
119+ assert (get_single_value () == value);
120+ }
121+
122+ private:
123+ NODISCARD UniqueBigSet &getUniqueBigSet () noexcept
124+ {
125+ assert (holds_nothing () || holds_unique_bigset ()); // used in assignment
126+ return *std::launder (reinterpret_cast <UniqueBigSet *>(m_buf));
127+ }
128+ NODISCARD const UniqueBigSet &getUniqueBigSet () const noexcept
129+ {
130+ assert (holds_unique_bigset ());
131+ return *std::launder (reinterpret_cast <const UniqueBigSet *>(m_buf));
132+ }
133+
134+ public:
135+ NODISCARD BigSet &get_big () noexcept
136+ {
137+ assert (holds_unique_bigset ());
138+ return *getUniqueBigSet ();
139+ }
140+ NODISCARD const BigSet &get_big () const noexcept
141+ {
142+ assert (holds_unique_bigset ());
143+ return *getUniqueBigSet ();
144+ }
145+ // newValue is allowed to be nullptr
146+ void assign_unique_bigset (UniqueBigSet newValue) noexcept
147+ {
148+ assert (holds_nothing () || holds_unique_bigset ());
149+ UniqueBigSet &ref = getUniqueBigSet ();
150+ using std::swap;
151+ swap (ref, newValue);
152+ assert (holds_nothing () || holds_unique_bigset ());
153+ }
154+ };
155+ Variant m_var;
32156
33157private:
34- NODISCARD bool isEmpty () const noexcept { return m_storage == 0u ; }
35- NODISCARD bool hasOne () const noexcept { return (m_storage & 0x1u ) == 0x1u ; }
36- NODISCARD bool hasBig () const noexcept { return ! isEmpty () && (m_storage & 0x1u ) == 0x0u ; }
158+ NODISCARD bool isEmpty () const noexcept { return m_var. holds_nothing () ; }
159+ NODISCARD bool hasOne () const noexcept { return m_var. holds_single_value () ; }
160+ NODISCARD bool hasBig () const noexcept { return m_var. holds_unique_bigset () ; }
37161
38162private:
39- NODISCARD Set &getBig ()
163+ NODISCARD BigSet &getBig ()
40164 {
41165 if (!hasBig ()) {
42166 throw std::runtime_error (" bad type" );
43167 }
44- return ** std::launder ( reinterpret_cast <PtrToSet *>(&m_storage) );
168+ return m_var. get_big ( );
45169 }
46- NODISCARD const Set &getBig () const
170+ NODISCARD const BigSet &getBig () const
47171 {
48172 if (!hasBig ()) {
49173 throw std::runtime_error (" bad type" );
50174 }
51- return ** std::launder ( reinterpret_cast < const PtrToSet *>(&m_storage) );
175+ return m_var. get_big ( );
52176 }
53177 NODISCARD Type getOnly () const
54178 {
55179 if (!hasOne ()) {
56180 throw std::runtime_error (" bad type" );
57181 }
58182
59- // NOTE: Only allows 31-bit roomids on 32-bit platforms.
60- return IdHelper_::fromBits (static_cast <size_t >(m_storage) >> 1u );
183+ return m_var.get_single_value ();
61184 }
62185
63186private:
64- void assign (PtrToSet newValue)
187+ void assign (UniqueBigSet newValue)
65188 {
66189 if (!hasBig ()) {
67- m_storage = 0 ;
190+ m_var. clear () ;
68191 }
69192
70- const bool wasNullptr = newValue == nullptr ;
71- PtrToSet *const ptr = std::launder (reinterpret_cast <PtrToSet *>(&m_storage));
72- using std::swap;
73- swap (*ptr, newValue);
193+ const bool should_expect_empty = newValue == nullptr ;
194+ m_var.assign_unique_bigset (std::exchange (newValue, {}));
74195
75- if (wasNullptr ) {
196+ if (should_expect_empty ) {
76197 assert (isEmpty ());
77198 } else {
78199 assert (hasBig ());
@@ -87,17 +208,13 @@ struct NODISCARD TinySet
87208 }
88209
89210 // NOTE: Only allows 31-bit roomids on 32-bit platforms.
90- m_storage = ( IdHelper_::getBits ( value) << 1u ) | 1u ;
211+ m_var. set_single_value ( value);
91212 assert (hasOne ());
92213 }
93214
94215 void clear () noexcept
95216 {
96- if (hasBig ()) {
97- assign (nullptr );
98- } else {
99- m_storage = 0 ;
100- }
217+ m_var.clear ();
101218 assert (isEmpty ());
102219 }
103220
@@ -111,22 +228,22 @@ struct NODISCARD TinySet
111228 }
112229
113230 ~TinySet () { clear (); }
114- TinySet (TinySet &&other) noexcept { std::swap (m_storage , other.m_storage ); }
231+ TinySet (TinySet &&other) noexcept { std::swap (m_var , other.m_var ); }
115232
116233 TinySet (const TinySet &other)
117234 {
118235 if (other.hasBig ()) {
119- assign (std::make_unique<Set >(other.getBig ()));
236+ assign (std::make_unique<BigSet >(other.getBig ()));
120237 } else {
121238 clear ();
122- m_storage = other.m_storage ;
239+ m_var = other.m_var ;
123240 }
124241 }
125242
126243 TinySet &operator =(TinySet &&other) noexcept
127244 {
128245 if (std::addressof (other) != this ) {
129- std::swap (m_storage , other.m_storage );
246+ std::swap (m_var , other.m_var );
130247 }
131248 return *this ;
132249 }
@@ -139,22 +256,13 @@ struct NODISCARD TinySet
139256 return *this ;
140257 }
141258
142- explicit TinySet (Set &&other)
143- {
144- if (other.empty ()) {
145- /* nop */
146- } else if (other.size () == 1 ) {
147- *this = TinySet (other.first ());
148- } else {
149- set (std::make_unique<Set>(std::move (other)));
150- }
151- }
259+ explicit TinySet (BigSet &&other) = delete;
152260
153261public:
154262 struct NODISCARD ConstIterator final
155263 {
156264 private:
157- using SetConstIt = typename Set ::ConstIterator;
265+ using SetConstIt = typename BigSet ::ConstIterator;
158266 SetConstIt m_setIt{};
159267 Type m_val{};
160268 enum class NODISCARD StateEnum : uint8_t { Empty, One, Big };
@@ -238,9 +346,7 @@ struct NODISCARD TinySet
238346 }
239347 NODISCARD ConstIterator end () const
240348 {
241- if (isEmpty ()) {
242- return ConstIterator{};
243- } else if (hasOne ()) {
349+ if (isEmpty () || hasOne ()) {
244350 return ConstIterator{};
245351 } else {
246352 return ConstIterator{getBig ().end ()};
@@ -273,7 +379,7 @@ struct NODISCARD TinySet
273379
274380 NODISCARD bool operator ==(const TinySet &rhs) const
275381 {
276- if (m_storage == rhs.m_storage ) {
382+ if (m_var == rhs.m_var ) {
277383 return true ;
278384 }
279385
@@ -342,14 +448,14 @@ struct NODISCARD TinySet
342448 return ;
343449 }
344450
345- // convert to Big
346- PtrToSet ptrToBig = std::make_unique<Set >();
347- auto &big = *ptrToBig ;
451+ // convert to BigSet
452+ UniqueBigSet uniqueBigSet = std::make_unique<BigSet >();
453+ auto &big = *uniqueBigSet ;
348454 big.insert (getOnly ());
349455 big.insert (id);
350456
351457 assert (hasOne ());
352- assign (std::exchange (ptrToBig , {})); // conversion happens here
458+ assign (std::exchange (uniqueBigSet , {})); // conversion to BigSet happens here
353459 assert (hasBig ());
354460 }
355461
0 commit comments