Dynamically Types Boxes#
- <amongoc/box.h> (header file)#
- <amongoc/box.hpp> (header file)#
This header defines types and functions for dealing with
amongoc_box
, a generic container of arbitrary values.
Types#
-
struct [[zero_initializable]] amongoc_box#
A type-erased boxed value of arbitrary type. The box manages storage for the boxed value and may contain a destructor function that will be executed when the box is destroyed.
- Zero-initialized:
[[zero_initializable]]
A zero-initializedamongoc_box
is equivalent toamongoc_nil
.- Header:
When a box parameter or return value is documented with
[[type(…)]]
, this specifies the type that is expected to be contained within the box. The special type[[type(nil)]]
refers to a theamongoc_nil
special box value.In C code, use
amongoc_box_init()
to initialize box objects.When working with C++ code, prefer to use
amongoc::unique_box
, as it will prevent accidental copying and ensures the destructor is executed on the box when it goes out of scope.Members
All
amongoc_box
data members other thanview
are private and should not be accessed or modified.-
amongoc_view view#
Accessing this member will yield an
amongoc_view
that refers to this box’s contents.Note
It is only legal to access this member if the box is active
-
as_unique() &&#
(C++ API) Move the box into an
amongoc::unique_box
to be managed automatically.
-
struct amongoc_view#
A read-only view of an
amongoc_box
value.- Header:
An
amongoc_view
\(V\) is valid if it was created by accessing theamongoc_box::view
member of an activeamongoc_box
\(B\), and is only valid as long the original \(B\) remains active. Consuming a box \(B\) will invalidate allamongoc_view
objects that were created from \(B\).-
template<typename T>
T &as() noexcept# The same as
amongoc::unique_box::as()
-
type __box_or_view#
A special exposition-only parameter type representing either an
amongoc_box
or anamongoc_view
.
-
using amongoc_box_destructor = void (*)(void *p)#
Type of the destructor function that may be associated with a box. The function parameter
p
is a pointer to the object that was stored within the box.After the destructor function is invoked, any dynamic storage associated with the box will be released.
-
using amongoc::box = ::amongoc_box#
amongoc::box
is a type alias of::amongoc_box
- Header:
-
class amongoc::unique_box#
(C++ API) Wraps an
amongoc_box
, restricting copying and ensuring destruction to prevent programmer error. Theunique_box
is move-only.- Header:
Note
unique_box
is not default-constructible. If you want a reasonable “nothing” box, usingamongoc::nil()
to initialize a new instance.-
unique_box(amongoc_box&&)#
Take ownership of the given box. The box must be passed as an rvalue-reference to emphasize this ownership transfer. The moved-from box will be overwritten with
amongoc_nil
.
-
~unique_box()#
Destroy the underlying box.
-
operator amongoc_view()#
Implicit conversion to an
amongoc_view
-
template<typename T>
T &as() noexcept# Obtain an lvalue reference to the contained value of type
T
.- Precondition:
The box must be active for the type
T
.- C API:
-
template<typename T>
static unique_box from(mlib::allocator<>, T &&value)# Construct a new
unique_box
by decay-copying from the given value. This should be the preferred way to create box objects within C++ code.- Throws:
std::bad_alloc – If memory allocation fails. This will never throw if the box is small.
- Postcondition:
The returned box object is active for the decayed type of
T
.
-
template<typename T, typename D>
static unique_box from(
)# Create a new box object by copying the given value and imbuing it with a destructor based on
D
. The typeT
must be trivially destructible, because the box will instead useD
as a destructor.In general, the given destructor should be a stateless function-object type (e.g. a lambda expression with no captures) that accepts a
T&
and destroys the object. Using anything else (e.g. a function pointer) will not work.- Throws:
std::bad_alloc – If memory allocation fails. This will never throw if the box is small.
- Postcondition:
requires std::is_trivially_destructible_v<T>
-
template<typename T, typename ...Args>
static unique_box make(
)# In-place construct a new instance of
T
into a new box.- Parameters:
a – The allocator to be used for the box.
args – Constructor arguments for the new
T
.
-
[[nodiscard]] amongoc_box release() && noexcept#
Relinquish ownership of the underlying box and return it to the caller. This function is used to interface with C APIs that will
[[transfer]]
anamongoc_box
by-value.
Functions & Macros#
Box Creation / Destruction#
-
amongoc_box_init(amongoc_box b, __type T)#
-
amongoc_box_init(amongoc_box b, __type T, amongoc_box_destructor dtor)#
- amongoc_box_init(
- amongoc_box b,
- __type T,
- amongoc_box_destructor dtor,
- mlib_allocator alloc,
-
amongoc_box_init_noinline(amongoc_box b, __type T)#
-
amongoc_box_init_noinline(amongoc_box b, __type T, amongoc_box_destructor dtor)#
- amongoc_box_init_noinline(
- amongoc_box b,
- __type T,
- amongoc_box_destructor dtor,
- mlib_allocator alloc,
Initialize a box to contain a zero-initialized storage for an instance of type
T
.- C++ API:
- Parameters:
b – An modifiable lvalue expression of type
amongoc_box
. This is the box that will be initiatlized.T – The type that should be stored within the box.
dtor – A destructor function that should be executed when the box is destroyed with
amongoc_box_destroy()
. The destructor function should be convertible to a function pointer:amongoc_box_destructor
. If omitted, the box will have no associated destructor.alloc – An
mlib_allocator
object to be used if the box requires dynamic allocation. If omitted, the default allocator will be used.
- Returns:
Returns a non-
const
pointer toT
. If memory allocation was required and fails, this returnsnullptr
. Note that a small type used withamongoc_box_init()
will not allocate, so the returned pointer in such a scenario will never be null.
The
_noinline
variant will inhibit the small-object optimization, which is required if the object being stored is not relocatable (i.e. it must be address-stable).
-
void amongoc_box_destroy(amongoc_box [[transfer]] b)#
Consume the given box and destroy its contents.
- Parameters:
b –
[[transfer]]
The box that will be consumed and whose contained value will be destroyed.
-
void amongoc_box_free_storage(amongoc_box [[transfer]] b)#
Note
Do not confuse this with
amongoc_box_destroy()
This function will release dynamically allocated storage associated with the given box without destroying the value that it may have contained.
This function should be used when the value within the box is moved-from, and the box itself is no longer needed.
Inspection#
-
T amongoc_box_cast(__type T, __box_or_view box)#
- Parameters:
T – The target type for the cast expression.
- C++ API:
Perform a cast from an
amongoc_box
oramongoc_view
to an lvalue expression of typeT
.Note that because the result is an lvalue expression, this cast expression can be used to manipulate the value stored in the box:
void changed_boxed_int(amongoc_box* b) { // Replace the boxed integer value with 42 amongoc_box_cast(int, *b) = 42; }
If the given box is not active for the type
T
, then the behavior is undefined.
-
void *amongoc_box_data(amongoc_box b)#
-
const void *amongoc_box_data(const amongoc_box b)#
-
const void *amongoc_box_data(amongoc_view b)#
Obtain a pointer to the object stored within a box
b
. Expands to an r-value of typevoid*
. Ifb
is aconst
box or anamongoc_view
, the returned pointer is a pointer-to-const
.- C++ API:
Note
This is implemented as a C preprocessor macro.
-
void amongoc_box_take(auto dest, amongoc_box [[transfer]] box)#
Moves the value stored in
box
to overwrite the objectdest
.- Parameters:
dest – A non-
const
lvalue expression of type \(T\) that will receive the boxed value.box –
[[transfer]]
A non-const
box that is active for the type \(T\).
This is useful to move an object from the type-erased box into a typed storage variable for more convenient access. The dynamic storage for
box
will be released, but the destructor for the box will not be executed. The object is now stored withindest
and it is up to the caller to manage its lifetime.Note
This is implemented as a C preprocessor macro.
Example
struct my_large_object { int values[64]; }; // ... void foo(amongoc_box large) { my_large_object o; amongoc_box_take(o, large); // `o` now has the value from `large`, and dynamic storage for `large` // has been released. }
Trivial Box Constructors#
-
amongoc_box amongoc_box_pointer(const void *x)#
-
amongoc_box amongoc_box_float(float x)#
-
amongoc_box amongoc_box_double(double x)#
-
amongoc_box amongoc_box_char(char x)#
-
amongoc_box amongoc_box_short(short x)#
-
amongoc_box amongoc_box_int(int x)#
-
amongoc_box amongoc_box_unsigned(unsigned int x)#
-
amongoc_box amongoc_box_long(long x)#
-
amongoc_box amongoc_box_ulong(unsigned long x)#
-
amongoc_box amongoc_box_longlong(long long x)#
-
amongoc_box amongoc_box_ulonglong(unsigned long long x)#
-
amongoc_box amongoc_box_size(size_t x)#
-
amongoc_box amongoc_box_ptrdiff(ptrdiff_t x)#
-
amongoc_box amongoc_box_int8(int8_t x)#
-
amongoc_box amongoc_box_uint8(uint8_t x)#
-
amongoc_box amongoc_box_int16(int16_t x)#
-
amongoc_box amongoc_box_uint16(uint16_t x)#
-
amongoc_box amongoc_box_int32(int32_t x)#
-
amongoc_box amongoc_box_uint32(uint32_t x)#
-
amongoc_box amongoc_box_int64(int64_t x)#
-
amongoc_box amongoc_box_uint64(uint64_t x)#
Convenience functions that initialize a new
amongoc_box
with the type and value of the given argument.Note that all of the boxes returned by these functions are trivial.
-
unique_box amongoc::nil() noexcept#
Returns a unique box containing no value.
- C API:
Constants#
-
const amongoc_box amongoc_nil#
A box value that contains no value. The resulting
amongoc_box
is trivial. Destroying a box constructed fromamongoc_nil
is a no-op.- C++ API:
Note
This is implemented as a C preprocessor macro.
-
template<typename T>
constexpr bool amongoc::enable_trivially_relocatable_v# Trait variable template that determines whether
amongoc::unique_box::from()
will try to store an object inline within a box (omitting allocation).- Header:
amongoc/relocation.hpp
By default any objects that are both trivially destructible and trivially move-constructible are considered to be trivially relocatable.
By the above rule all of the following are considered trivially relocatable:
All built-in language types
All pointer types
All class types that have no non-trivial move/destroy operations (including all pure C structs)
C++ closure objects that have no non-trivial move/destroy operations (this is based on the type of values that it captures).
Additionally, if the type
T
has a nested typeenable_trivially_relocatable
that is defined toT
, then the object will be treated as trivially relocatable.
Box Behavior#
At any given time, an amongoc_box
is either active for type T, or dead.
State: Active for type \(T\)#
A box \(B\) is active for type \(T\) if either:
\(B\) was used with
amongoc_box_init()
/amongoc_box_init_noinline()
with the type \(T\)OR \(B\) was created with a C++ API that constructs a box,
OR \(B\) is a by-value copy of an
amongoc_box
that was already active for type \(T\).
AND:
\(B\) has not been consumed by any operation (i.e. passed through a
[[transfer]]
parameter)
If a box is active for type \(T\), then it is legal to use it in
amongoc_box_cast()
with type \(T\).
State: Dead#
A box \(B\) is dead if either:
\(B\) is newly declared and uninitialized.
or \(B\) was used in any operation that consumed it.
Consuming Operations#
A box \(B\) is consumed by any of the following operations:
Passing \(B\) by-value to any function parameter marked with
[[transfer]]
.Returning \(B\) by-value from a function.
Copy-assigning \(B\) into another l-value expression of type
amongoc_box
.
Relocation#
The amongoc_box
should be considered trivially relocatable. That is: A
byte-wise copy of the object can be considered a moved-to amongoc_box
,
invalidating the box that was copied-from (i.e.
consuming it).
Smallness#
amongoc_box
considers some objects to be “small”. If those objects are small,
then it is guaranteed that amongoc_box
will not allocate memory for storing
those objects.
The only types guaranteed to be considered “small” are objects no larger than two pointers.
Non-Relocatable Types#
To store an object that cannot be trivially relocated within an amongoc_box
,
one should use amongoc_box_init_noinline()
, which forcibly disables the
small-object optimization within the created box.
The C++ APIs amongoc::unique_box::from()
will automatically handle this
distiction by consulting amongoc::enable_trivially_relocatable_v
.
Triviallity#
An amongoc_box
is said to be trivial if the type it contains is
small and the box has no associated destructor.
When a box is trivial, some usage requirements relax:
A trivial box may be copied arbitrarily without invalidating other copies, and each copy has a distinct identity.
It is safe to discard a trivial box (allow it to leave scope) without ever calling
amongoc_box_destroy()
.It is safe to overwrite or reinitialize the box (e.g.
amongoc_box_init()
) with a new value without first destroying the box.
In general: the semantics of the [[transfer]]
attribute do not apply to
trivial boxes.
Note
It is not sufficient that the box is simply small or contains a primitive
type: It is possible that such a box has a destructor that needs to execute on
the primitive’s value (e.g. POSIX close
is a destructor for an int
).
Storage Alignment#
Important
At the current time, boxes allocate and store values using the default maximum-alignment defined by the compiler. There is not yet support for types that require additional alignment.
Q: “Can I query the state of a box?”#
In general, no. The properties of a box (i.e. type, state, triviallity, smallness, whether it is nil, and whether it has a destructor) are stored as implementation details. Code should be designed to treat all live boxes as non-trivial unless they are known to be otherwise.
Attributes of boxes may be carried in other channels, e.g through an associated
amongoc_status
parameter, but it is up to the particular box+status pair to
define the semantics thereof.