@@ -18,61 +18,185 @@ 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+ // copy is unnecessary but should be optimized away;
100+ // it's only here as a defense mechanism.
101+ assert (holds_single_value ());
102+ const Variant copy = *this ;
103+ const auto ui = *std::launder (reinterpret_cast <const uintptr_t *>(copy.m_buf )) >> 1u ;
104+ const auto size = static_cast <size_t >(ui);
105+ return IdHelper_::fromBits (size);
106+ }
107+ void set_single_value (const Type value) noexcept
108+ {
109+ if (holds_unique_bigset ()) {
110+ assert (!holds_unique_bigset ());
111+ std::abort (); // crash
112+ }
113+
114+ constexpr size_t MASK = ~size_t {0 } >> 1u ;
115+ const auto raw_val = static_cast <size_t >(IdHelper_::getBits (value));
116+ const auto masked_val = static_cast <uintptr_t >(raw_val & MASK);
117+ assert (raw_val == masked_val);
118+ const auto ui = (masked_val << 1u ) | 1u ;
119+ assert (holds_nothing () || holds_single_value ());
120+ *std::launder (reinterpret_cast <uintptr_t *>(m_buf)) = ui;
121+ assert (holds_single_value ());
122+ assert (get_single_value () == value);
123+ }
124+
125+ private:
126+ NODISCARD UniqueBigSet &getUniqueBigSet () noexcept
127+ {
128+ assert (holds_nothing () || holds_unique_bigset ()); // used in assignment
129+ return *std::launder (reinterpret_cast <UniqueBigSet *>(m_buf));
130+ }
131+ NODISCARD const UniqueBigSet &getUniqueBigSet () const noexcept
132+ {
133+ assert (holds_unique_bigset ());
134+ return *std::launder (reinterpret_cast <const UniqueBigSet *>(m_buf));
135+ }
136+
137+ public:
138+ NODISCARD BigSet &get_big () noexcept
139+ {
140+ assert (holds_unique_bigset ());
141+ return *getUniqueBigSet ();
142+ }
143+ NODISCARD const BigSet &get_big () const noexcept
144+ {
145+ assert (holds_unique_bigset ());
146+ return *getUniqueBigSet ();
147+ }
148+ // newValue is allowed to be nullptr
149+ void assign_unique_bigset (UniqueBigSet newValue) noexcept
150+ {
151+ assert (holds_nothing () || holds_unique_bigset ());
152+ UniqueBigSet &ref = getUniqueBigSet ();
153+ using std::swap;
154+ swap (ref, newValue);
155+ assert (holds_nothing () || holds_unique_bigset ());
156+ }
157+ };
158+ Variant m_var;
32159
33160private:
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 ; }
161+ NODISCARD bool isEmpty () const noexcept { return m_var. holds_nothing () ; }
162+ NODISCARD bool hasOne () const noexcept { return m_var. holds_single_value () ; }
163+ NODISCARD bool hasBig () const noexcept { return m_var. holds_unique_bigset () ; }
37164
38165private:
39- NODISCARD Set &getBig ()
166+ NODISCARD BigSet &getBig ()
40167 {
41168 if (!hasBig ()) {
42169 throw std::runtime_error (" bad type" );
43170 }
44- return ** std::launder ( reinterpret_cast <PtrToSet *>(&m_storage) );
171+ return m_var. get_big ( );
45172 }
46- NODISCARD const Set &getBig () const
173+ NODISCARD const BigSet &getBig () const
47174 {
48175 if (!hasBig ()) {
49176 throw std::runtime_error (" bad type" );
50177 }
51- return ** std::launder ( reinterpret_cast < const PtrToSet *>(&m_storage) );
178+ return m_var. get_big ( );
52179 }
53180 NODISCARD Type getOnly () const
54181 {
55182 if (!hasOne ()) {
56183 throw std::runtime_error (" bad type" );
57184 }
58185
59- // NOTE: Only allows 31-bit roomids on 32-bit platforms.
60- return IdHelper_::fromBits (static_cast <size_t >(m_storage) >> 1u );
186+ return m_var.get_single_value ();
61187 }
62188
63189private:
64- void assign (PtrToSet newValue)
190+ void assign (UniqueBigSet newValue)
65191 {
66192 if (!hasBig ()) {
67- m_storage = 0 ;
193+ m_var. clear () ;
68194 }
69195
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);
196+ const bool should_expect_empty = newValue == nullptr ;
197+ m_var.assign_unique_bigset (std::exchange (newValue, {}));
74198
75- if (wasNullptr ) {
199+ if (should_expect_empty ) {
76200 assert (isEmpty ());
77201 } else {
78202 assert (hasBig ());
@@ -87,17 +211,13 @@ struct NODISCARD TinySet
87211 }
88212
89213 // NOTE: Only allows 31-bit roomids on 32-bit platforms.
90- m_storage = ( IdHelper_::getBits ( value) << 1u ) | 1u ;
214+ m_var. set_single_value ( value);
91215 assert (hasOne ());
92216 }
93217
94218 void clear () noexcept
95219 {
96- if (hasBig ()) {
97- assign (nullptr );
98- } else {
99- m_storage = 0 ;
100- }
220+ m_var.clear ();
101221 assert (isEmpty ());
102222 }
103223
@@ -111,22 +231,22 @@ struct NODISCARD TinySet
111231 }
112232
113233 ~TinySet () { clear (); }
114- TinySet (TinySet &&other) noexcept { std::swap (m_storage , other.m_storage ); }
234+ TinySet (TinySet &&other) noexcept { std::swap (m_var , other.m_var ); }
115235
116236 TinySet (const TinySet &other)
117237 {
118238 if (other.hasBig ()) {
119- assign (std::make_unique<Set >(other.getBig ()));
239+ assign (std::make_unique<BigSet >(other.getBig ()));
120240 } else {
121241 clear ();
122- m_storage = other.m_storage ;
242+ m_var = other.m_var ;
123243 }
124244 }
125245
126246 TinySet &operator =(TinySet &&other) noexcept
127247 {
128248 if (std::addressof (other) != this ) {
129- std::swap (m_storage , other.m_storage );
249+ std::swap (m_var , other.m_var );
130250 }
131251 return *this ;
132252 }
@@ -139,22 +259,13 @@ struct NODISCARD TinySet
139259 return *this ;
140260 }
141261
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- }
262+ explicit TinySet (BigSet &&other) = delete;
152263
153264public:
154265 struct NODISCARD ConstIterator final
155266 {
156267 private:
157- using SetConstIt = typename Set ::ConstIterator;
268+ using SetConstIt = typename BigSet ::ConstIterator;
158269 SetConstIt m_setIt{};
159270 Type m_val{};
160271 enum class NODISCARD StateEnum : uint8_t { Empty, One, Big };
@@ -273,7 +384,7 @@ struct NODISCARD TinySet
273384
274385 NODISCARD bool operator ==(const TinySet &rhs) const
275386 {
276- if (m_storage == rhs.m_storage ) {
387+ if (m_var == rhs.m_var ) {
277388 return true ;
278389 }
279390
@@ -342,14 +453,14 @@ struct NODISCARD TinySet
342453 return ;
343454 }
344455
345- // convert to Big
346- PtrToSet ptrToBig = std::make_unique<Set >();
347- auto &big = *ptrToBig ;
456+ // convert to BigSet
457+ UniqueBigSet uniqueBigSet = std::make_unique<BigSet >();
458+ auto &big = *uniqueBigSet ;
348459 big.insert (getOnly ());
349460 big.insert (id);
350461
351462 assert (hasOne ());
352- assign (std::exchange (ptrToBig , {})); // conversion happens here
463+ assign (std::exchange (uniqueBigSet , {})); // conversion to BigSet happens here
353464 assert (hasBig ());
354465 }
355466
0 commit comments