Skip to content

Commit 04947cd

Browse files
committed
fix UB revealed by gcc release build
1 parent 7923eac commit 04947cd

1 file changed

Lines changed: 160 additions & 54 deletions

File tree

src/map/TinyRoomIdSet.h

Lines changed: 160 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -18,61 +18,182 @@ struct NODISCARD TinySet
1818
{
1919
private:
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

3024
private:
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

33157
private:
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

38162
private:
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

63186
private:
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

153261
public:
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

Comments
 (0)