Rerun C++ SDK
Loading...
Searching...
No Matches
collection.hpp
1#pragma once
2
3#include <algorithm>
4#include <cassert>
5#include <cstdint>
6#include <cstring> // std::memset
7#include <utility>
8#include <vector>
9
10#include "collection.hpp"
11#include "collection_adapter.hpp"
12#include "compiler_utils.hpp"
13
14namespace rerun {
15 /// Type of ownership of a collection's data.
16 ///
17 /// User access to this is typically only needed for debugging and testing.
19 /// The collection does not own the data and only has a pointer and a size.
21
22 /// The collection batch owns the data via an std::vector.
24 };
25
26 /// Generic collection of elements that are roughly contiguous in memory.
27 ///
28 /// The most notable feature of the `rerun::Collection` is that its data may be either **owned** or **borrowed**:
29 /// * Borrowed: ⚠️ If data is borrowed it *must* outlive its source ⚠️
30 /// (in particular, the pointer to the source mustn't invalidate)
31 /// * Owned: Owned data is copied into an internal std::vector
32 ///
33 /// Collections are either filled explicitly using `Collection::borrow` &`Collection::take_ownership`
34 /// or (most commonly in user code) implicitly using the `CollectionAdapter` trait
35 /// (see documentation for `CollectionAdapter` for more information on how data can be adapted).
36 ///
37 /// ⚠️ To ensure that passed data is not destroyed, move it into the collection using `std::move`.
38 ///
39 /// Other than being assignable, collections are generally immutable:
40 /// there is no mutable data access in order to not violate the contract with the data lender
41 /// and changes in size are not possible.
42 ///
43 /// ## Implementation notes:
44 ///
45 /// Does intentionally not implement copy construction since for the owned case this may
46 /// be expensive. Typically, there should be no need to copy rerun collections, so this more
47 /// than likely indicates a bug inside the Rerun SDK.
48 template <typename TElement>
49 class Collection {
50 public:
51 /// Type of the elements in the collection.
52 ///
53 /// Note that calling this `value_type` makes it compatible with the STL.
54 using value_type = TElement;
55
56 /// Type of an adapter given an input container type.
57 ///
58 /// Note that the "container" passed may also be a single element of something.
59 /// The only thing relevant is that there's an Adapter for it.
60 template <typename TContainer>
62 TElement, std::remove_cv_t<std::remove_reference_t<TContainer>>,
63 std::enable_if_t<true>>;
64
65 /// Creates a new empty collection.
67 storage.borrowed.data = nullptr;
68 storage.borrowed.num_instances = 0;
69 }
70
71 /// Construct using a `CollectionAdapter` for the given input type.
72 template <
73 typename TContainer, //
74 // Avoid conflicting with the copy/move constructor.
75 // We could implement this also with an adapter, but this might confuse trait checks like `std::is_copy_constructible`.
76 typename = std::enable_if_t<
77 !std::is_same_v<std::remove_reference_t<TContainer>, Collection<TElement>>> //
78 >
79 Collection(TContainer&& input)
80 : Collection(Adapter<TContainer>()(std::forward<TContainer>(input))) {}
81
82 /// Copy constructor.
83 ///
84 /// If the data is owned, this will copy the data.
85 /// If the data is borrowed, this will copy the borrow,
86 /// meaning there's now (at least) two collections borrowing the same data.
87 Collection(const Collection<TElement>& other) : ownership(other.ownership) {
88 switch (other.ownership) {
90 storage.borrowed = other.storage.borrowed;
91 break;
92 }
93
95 new (&storage.vector_owned) std::vector<TElement>(other.storage.vector_owned);
96 break;
97 }
98
99 default:
100 assert(false && "unreachable");
101 }
102 }
103
104 /// Copy assignment.
105 ///
106 /// If the data is owned, this will copy the data.
107 /// If the data is borrowed, this will copy the borrow,
108 /// meaning there's now (at least) two collections borrowing the same data.
109 void operator=(const Collection<TElement>& other) {
110 // Self-assignment would destroy `other` before copying from it.
111 if (this == &other) {
112 return;
113 }
114 this->~Collection<TElement>();
115 new (this) Collection(other);
116 }
117
118 /// Move constructor.
120 swap(other);
121 }
122
123 /// Move assignment.
125 // Need to disable the maybe-uninitialized here. It seems like the compiler may be confused in situations where
126 // we are assigning into an unused optional from a temporary. The fact that this hits the move-assignment without
127 // having called the move constructor is suspicious though and hints of an actual bug.
128 //
129 // See: https://github.com/rerun-io/rerun/issues/4027
130 RR_WITH_MAYBE_UNINITIALIZED_DISABLED(this->swap(other);)
131 }
132
133 /// Construct from a initializer list of elements that are compatible with TElement.
134 ///
135 /// Takes ownership of the passed elements.
136 /// If you want to avoid an allocation, you have to manually keep the data on the stack
137 /// (e.g. as `std::array`) and construct the collection from this instead.
138 ///
139 /// This is not done as a `CollectionAdapter` since it tends to cause deduction issues
140 /// (since there's special rules for overload resolution for initializer lists)
141 Collection(std::initializer_list<TElement> data)
142 : ownership(CollectionOwnership::VectorOwned) {
143 // Don't assign, since the vector is in an undefined state and assigning may
144 // attempt to free data.
145 new (&storage.vector_owned) std::vector<TElement>(data);
146 }
147
148 /// Borrows binary compatible data into the collection from a typed pointer.
149 ///
150 /// Borrowed data must outlive the collection!
151 /// (If the pointer passed is into an std::vector or similar, this std::vector mustn't be
152 /// resized.)
153 /// The passed type must be binary compatible with the collection type.
154 ///
155 /// Since `rerun::Collection` does not provide write access, data is guaranteed to be unchanged by
156 /// any function or operation taking on a `Collection`.
157 template <typename T>
158 static Collection<TElement> borrow(const T* data, size_t num_instances = 1) {
159 static_assert(
160 sizeof(T) == sizeof(TElement),
161 "T & TElement are not binary compatible: Size mismatch."
162 );
163 static_assert(
164 alignof(T) <= alignof(TElement),
165 "T & TElement are not binary compatible: TElement has a higher alignment requirement than T. This implies that pointers to T may not have the alignment needed to access TElement."
166 );
167
169 batch.ownership = CollectionOwnership::Borrowed;
170 batch.storage.borrowed.data = reinterpret_cast<const TElement*>(data);
171 batch.storage.borrowed.num_instances = num_instances;
172 return batch;
173 }
174
175 /// Borrows binary compatible data into the collection from an untyped pointer.
176 ///
177 /// This version of `borrow` that takes a void pointer, omitting any checks.
178 ///
179 /// Borrowed data must outlive the collection!
180 /// (If the pointer passed is into an std::vector or similar, this std::vector mustn't be
181 /// resized.)
182 ///
183 /// Since `rerun::Collection` does not provide write access, data is guaranteed to be unchanged by
184 /// any function or operation taking on a `rerun::Collection`.
185 static Collection borrow(const void* data, size_t num_instances = 1) {
186 return borrow(reinterpret_cast<const TElement*>(data), num_instances);
187 }
188
189 /// Borrows binary compatible data into the collection from a vector.
190 ///
191 /// Borrowed data must outlive the collection!
192 /// The referenced vector must not be resized and mustn't be temporary.
193 ///
194 /// Since `rerun::Collection` does not provide write access, data is guaranteed to be unchanged by
195 /// any function or operation taking on a `rerun::Collection`.
196 template <typename T>
197 static Collection borrow(const std::vector<T>& data) {
198 return borrow(data.data(), data.size());
199 }
200
201 /// Takes ownership of a temporary `std::vector`, moving it into the collection.
202 ///
203 /// Takes ownership of the data and moves it into the collection.
204 static Collection<TElement> take_ownership(std::vector<TElement>&& data) {
206 batch.ownership = CollectionOwnership::VectorOwned;
207 // Don't assign, since the vector is in an undefined state and assigning may
208 // attempt to free data.
209 new (&batch.storage.vector_owned) std::vector<TElement>(std::move(data));
210
211 return batch;
212 }
213
214 /// Takes ownership of a single element, moving it into the collection.
216 // TODO(#4256): there should be a special path here to avoid allocating a vector.
217 std::vector<TElement> elements;
218 elements.emplace_back(std::move(data));
219 return take_ownership(std::move(elements));
220 }
221
222 /// Takes ownership of a single element, copying it into the collection.
223 static Collection<TElement> take_ownership(const TElement& data) {
224 // TODO(#4256): there should be a special path here to avoid allocating a vector.
225 std::vector<TElement> elements = {data};
226 return take_ownership(std::move(elements));
227 }
228
229 /// Swaps the content of this collection with another.
231 // (writing out this-> here to make it less confusing!)
232 switch (this->ownership) {
234 switch (other.ownership) {
236 std::swap(this->storage.borrowed, other.storage.borrowed);
237 break;
238
240 auto this_borrowed_data_old = this->storage.borrowed;
241 new (&this->storage.vector_owned)
242 std::vector<TElement>(std::move(other.storage.vector_owned));
243 other.storage.borrowed = this_borrowed_data_old;
244 break;
245 }
246
247 default:
248 assert(false && "unreachable");
249 }
250 break;
251 }
252
254 switch (other.ownership) {
256 auto other_borrowed_data_old = other.storage.borrowed;
257 new (&other.storage.vector_owned)
258 std::vector<TElement>(std::move(this->storage.vector_owned));
259 this->storage.borrowed = other_borrowed_data_old;
260 break;
261 }
262
264 std::swap(storage.vector_owned, other.storage.vector_owned);
265 break;
266
267 default:
268 assert(false && "unreachable");
269 }
270 break;
271 }
272
273 default:
274 assert(false && "unreachable");
275 }
276
277 std::swap(ownership, other.ownership);
278 }
279
280 ~Collection() {
281 switch (ownership) {
283 break; // nothing to do.
284
286 storage.vector_owned.~vector(); // Deallocate the vector!
287 break;
288
289 default:
290 assert(false && "unreachable");
291 }
292 }
293
294 /// Returns the number of instances in this collection.
295 size_t size() const {
296 switch (ownership) {
298 return storage.borrowed.num_instances;
299
301 return storage.vector_owned.size();
302
303 default:
304 assert(false && "unreachable");
305 }
306 return 0;
307 }
308
309 /// Returns true if the collection is empty.
310 bool empty() const {
311 return size() == 0;
312 }
313
314 /// Returns a raw pointer to the underlying data.
315 ///
316 /// Do not use this if the data is not continuous in memory!
317 /// TODO(#4257): So far it always is continuous, but in the future we want to support strides!
318 ///
319 /// The pointer is only valid as long as backing storage is alive
320 /// which is either until the collection is destroyed the borrowed source is destroyed/moved.
321 const TElement* data() const {
322 switch (ownership) {
324 return storage.borrowed.data;
325
327 return storage.vector_owned.data();
328
329 default:
330 assert(false && "unreachable");
331 }
332
333 // We need to return something to avoid compiler warnings.
334 // But if we don't mark this as unreachable, GCC will complain that we're dereferencing null down the line.
335 RR_UNREACHABLE();
336 // But with this in place, MSVC complains that the return statement is not reachable (GCC/clang on the other hand need it).
337#ifndef _MSC_VER
338 return nullptr;
339#endif
340 }
341
342 /// TODO(andreas): Return proper iterator
343 const TElement* begin() const {
344 return data();
345 }
346
347 /// TODO(andreas): Return proper iterator
348 const TElement* end() const {
349 return data() + size();
350 }
351
352 /// Random read access to the underlying data.
353 const TElement& operator[](size_t i) const {
354 assert(i < size());
355 return data()[i];
356 }
357
358 /// Returns the data ownership of collection.
359 ///
360 /// This is usually only needed for debugging and testing.
362 return ownership;
363 }
364
365 /// Copies the data into a new `std::vector`.
366 std::vector<TElement> to_vector() const& {
367 return std::vector<TElement>(begin(), end());
368 }
369
370 /// Copies the data into a new `std::vector`.
371 ///
372 /// If possible, this will move the underlying data.
373 std::vector<TElement> to_vector() && {
374 switch (ownership) {
376 std::vector<TElement> result;
377 result.reserve(size());
378 result.insert(result.end(), begin(), end());
379 return result;
380 }
381
383 // Ensure move constructor is called, so `storage.vector_owned` is in a valid state.
384 return std::move(storage.vector_owned);
385 }
386
387 default:
388 assert(false && "unreachable");
389 }
390 return std::vector<TElement>();
391 }
392
393 /// Reinterpret this collection as a collection of bytes.
395 switch (ownership) {
398 reinterpret_cast<const uint8_t*>(data()),
399 size() * sizeof(TElement)
400 );
401 }
402
404 auto ptr = reinterpret_cast<const uint8_t*>(data());
405 auto num_bytes = size() * sizeof(TElement);
407 std::vector<uint8_t>(ptr, ptr + num_bytes)
408 );
409 }
410
411 default:
412 assert(false && "unreachable");
413 }
414 return Collection<uint8_t>();
415 }
416
417 private:
418 template <typename T>
419 union CollectionStorage {
420 struct {
421 const T* data;
422 size_t num_instances;
423 } borrowed;
424
425 std::vector<T> vector_owned;
426
427 CollectionStorage() {
428 std::memset(reinterpret_cast<void*>(this), 0, sizeof(CollectionStorage));
429 }
430
431 ~CollectionStorage() {}
432 };
433
434 CollectionOwnership ownership;
435 CollectionStorage<TElement> storage;
436 };
437
438 // Convenience functions for creating typed collections via explicit borrow & ownership taking.
439 // These are useful to avoid having to specify the type of the collection.
440 // E.g. instead of `rerun::Collection<uint8_t>::borrow(data, num_instances)`,
441 // you can just write `rerun::borrow(data, num_instances)`.
442
443 /// Borrows binary data into a `Collection` from a pointer.
444 ///
445 /// Borrowed data must outlive the collection!
446 /// (If the pointer passed is into an std::vector or similar, this std::vector mustn't be
447 /// resized.)
448 /// The passed type must be binary compatible with the collection type.
449 ///
450 /// Since `rerun::Collection` does not provide write access, data is guaranteed to be unchanged by
451 /// any function or operation taking on a `Collection`.
452 template <typename TElement>
453 inline Collection<TElement> borrow(const TElement* data, size_t num_instances = 1) {
454 return Collection<TElement>::borrow(data, num_instances);
455 }
456
457 /// Borrows binary data into the collection from a vector.
458 ///
459 /// Borrowed data must outlive the collection!
460 /// The referenced vector must not be resized and mustn't be temporary.
461 ///
462 /// Since `rerun::Collection` does not provide write access, data is guaranteed to be unchanged by
463 /// any function or operation taking on a `rerun::Collection`.
464 template <typename TElement>
465 inline Collection<TElement> borrow(const std::vector<TElement>& data) {
466 return Collection<TElement>::borrow(data);
467 }
468
469 /// Takes ownership of a temporary `std::vector`, moving it into the collection.
470 ///
471 /// Takes ownership of the data and moves it into the collection.
472 template <typename TElement>
473 inline Collection<TElement> take_ownership(std::vector<TElement> data) {
474 return Collection<TElement>::take_ownership(std::move(data));
475 }
476
477 /// Takes ownership of a single element, moving it into the collection.
478 template <typename TElement>
479 inline Collection<TElement> take_ownership(TElement data) {
480 return Collection<TElement>::take_ownership(std::move(data));
481 }
482} // namespace rerun
483
484// Could keep this separately, but its very hard to use the collection without the basic suite of adapters.
485// Needs to know about `rerun::Collection` which means that it needs to be included after `rerun::Collection` is defined.
486// (it tried to include `Collection.hpp` but if that was our starting point that include wouldn't do anything)
487#include "collection_adapter_builtins.hpp"
Generic collection of elements that are roughly contiguous in memory.
Definition collection.hpp:49
const TElement & operator[](size_t i) const
Random read access to the underlying data.
Definition collection.hpp:353
static Collection< TElement > take_ownership(TElement &&data)
Takes ownership of a single element, moving it into the collection.
Definition collection.hpp:215
static Collection< TElement > borrow(const T *data, size_t num_instances=1)
Borrows binary compatible data into the collection from a typed pointer.
Definition collection.hpp:158
static Collection< TElement > take_ownership(std::vector< TElement > &&data)
Takes ownership of a temporary std::vector, moving it into the collection.
Definition collection.hpp:204
bool empty() const
Returns true if the collection is empty.
Definition collection.hpp:310
Collection(Collection< TElement > &&other)
Move constructor.
Definition collection.hpp:119
static Collection borrow(const std::vector< T > &data)
Borrows binary compatible data into the collection from a vector.
Definition collection.hpp:197
Collection()
Creates a new empty collection.
Definition collection.hpp:66
CollectionOwnership get_ownership() const
Returns the data ownership of collection.
Definition collection.hpp:361
const TElement * end() const
TODO(andreas): Return proper iterator.
Definition collection.hpp:348
void operator=(const Collection< TElement > &other)
Copy assignment.
Definition collection.hpp:109
const TElement * begin() const
TODO(andreas): Return proper iterator.
Definition collection.hpp:343
void swap(Collection< TElement > &other)
Swaps the content of this collection with another.
Definition collection.hpp:230
TElement value_type
Type of the elements in the collection.
Definition collection.hpp:54
static Collection< TElement > take_ownership(const TElement &data)
Takes ownership of a single element, copying it into the collection.
Definition collection.hpp:223
size_t size() const
Returns the number of instances in this collection.
Definition collection.hpp:295
void operator=(Collection< TElement > &&other)
Move assignment.
Definition collection.hpp:124
static Collection borrow(const void *data, size_t num_instances=1)
Borrows binary compatible data into the collection from an untyped pointer.
Definition collection.hpp:185
Collection(TContainer &&input)
Construct using a CollectionAdapter for the given input type.
Definition collection.hpp:79
Collection(const Collection< TElement > &other)
Copy constructor.
Definition collection.hpp:87
Collection(std::initializer_list< TElement > data)
Construct from a initializer list of elements that are compatible with TElement.
Definition collection.hpp:141
std::vector< TElement > to_vector() const &
Copies the data into a new std::vector.
Definition collection.hpp:366
Collection< uint8_t > to_uint8() const
Reinterpret this collection as a collection of bytes.
Definition collection.hpp:394
std::vector< TElement > to_vector() &&
Copies the data into a new std::vector.
Definition collection.hpp:373
const TElement * data() const
Returns a raw pointer to the underlying data.
Definition collection.hpp:321
All Rerun C++ types and functions are in the rerun namespace or one of its nested namespaces.
Definition rerun.hpp:23
Collection< TElement > take_ownership(std::vector< TElement > data)
Takes ownership of a temporary std::vector, moving it into the collection.
Definition collection.hpp:473
CollectionOwnership
Type of ownership of a collection's data.
Definition collection.hpp:18
@ Borrowed
The collection does not own the data and only has a pointer and a size.
@ VectorOwned
The collection batch owns the data via an std::vector.
Collection< TElement > borrow(const TElement *data, size_t num_instances=1)
Borrows binary data into a Collection from a pointer.
Definition collection.hpp:453
The rerun::CollectionAdapter trait is responsible for mapping an input argument to a rerun::Collectio...
Definition collection_adapter.hpp:25