A C++17 library to emulate C flexible array members. See the User Guide for a complete walkthrough.
Consider the following implementation of a Node
in a graph structure:
struct Node
{
std::size_t id;
void* userData {nullptr};
std::unique_ptr<Node*[]> links;
};
Node* makeGraphNode(std::size_t id, const std::vector<Node*>& links)
{
auto n = new Node{id};
n->links = std::make_unique<Node*[]>(links.size());
std::copy(links.begin(), links.end(), n->links.get());
return n;
}
In this scenario, everytime makeGraphNode
is called two allocations will occur:
- One for
links
array - One for
Node
object itself
Multiple allocations can be costly and incur in memory fragmentation which reduce cache friendlyness.
The same Node
can be implemented with Flexclass
:
struct Node
{
auto fc_handles() { return fc::make_tuple(&links); }
std::size_t id;
void* userData {nullptr};
fc::Array<Node*> links;
};
Node* makeGraphNode(std::size_t id, const std::vector<Node*>& links)
{
auto n = fc::make<Node>(links.size()) (id);
std::copy(links.begin(), links.end(), n->links.begin());
return n;
}
The required changes are:
- Declare
fc_handles
method that returns the required handles - Declare
links
asfc::Array<Node*>
which is a special handle for an array - Construct the object with the syntax
fc::make< T > ( array sizes ) ( T constructor arguments );
See this example on Compiler Explorer
To build the layout, Flexclass
uses fc_handles
to collect a list of array types to be built.
The layout of the Node
is:
_______
| |
| V
| [std::size_t] [void*] [Node**] | [Node*] [Node*] [Node*] ... [Node*]
| Id UserData Links |
| |
| Node |
Now a single allocation is performed to store both the Node
and the array of Node*
.
One can reduce the size even more by using the fact that the Node*
array will always be at the end of the Node
:
fc::AdjacentArray<Node*> links;
Which would generate the following compact layout:
|[std::size_t] [void*] [] | [Node*] [Node*] [Node*] ... [Node*]
| Id UserData Links |
| |
| Node |
Flexclass
is not limited to one array, so the following declaration is perfectly valid (godbolt link):
struct MyType
{
auto fc_handles() { return fc::make_tuple(&a, &c); }
fc::Array<int> a;
std::string b;
fc::Range<string> c;
bool d;
};
Which will generate the following layout:
_______________________________________________________________
| |
____________|_________________________________ |
| | | |
________________________|____________|_________________ | |
| | | V V V
|[int*] [std::string] [std::string*] [std::string*] [bool]| [int] ... [int] [std::string] ... [std::string]
| a b c (begin) c (end) d |
| |
| MyType |
Notice that fc::Range
has an end
pointer for the std::string
array. Since std::string
is non-trivially-destructible, Flexclass
needs to iterate on the array to call destructors when destroying the MyType
.
Range<T>
requests pointers to bothbegin
andend
of the array (cost: 2 pointers)Array<T>
requests a pointer only to thebegin
of the array (cost: 1 pointer)
Adjacent handles deduce their begin
from another element:
- By default, they are adjacent to the base
- They can also be adjacent to another array. See the user guide for the syntax.
Cost:
AdjacentArray<T>
cost 0 pointersAdjacentRange<T>
cost 1 pointer
A shared array implementation needs a reference counter and the data (in this case an array). So it can be modeled as:
template<class T> class SharedArray;
template<class T>
class SharedArray<T[]>
{
struct Impl
{
auto fc_handles() { return fc::make_tuple(&data); }
unsigned refCount;
std::conditional_t<std::is_trivially_destructible_v<T>,
fc::Array<T>,
fc::Range<T>> data;
};
public:
/* Interesting public API */
static SharedArray make(std::size_t len) { return {fc::make<Impl>(len)(/*num references*/1u)}; }
decltype(auto) operator[](std::size_t i) { return m_data->data.begin()[i]; }
decltype(auto) operator[](std::size_t i) const { return m_data->data.begin()[i]; }
auto use_count() const { return m_data ? m_data->refCount : 0; }
/* Boilerplate */
SharedArray(SharedArray&& other) : m_data(std::exchange(other.m_data, nullptr)) {}
SharedArray(const SharedArray& other) { m_data = other.m_data; incr(); }
SharedArray& operator=(SharedArray&& other) { decr(); m_data = std::exchange(other.m_data, nullptr); return *this; }
SharedArray& operator=(const SharedArray& other) { decr(); m_data = other.m_data; incr(); return *this; }
~SharedArray() { decr(); }
private:
SharedArray(Impl* data) : m_data(data) {}
void incr() { if (m_data) m_data->refCount++; }
void decr() { if (m_data && m_data->refCount-- == 1) fc::destroy(m_data); }
Impl* m_data {nullptr};
};
- Provide ways to convert from a Adjacent member to base
- Add range-for support for AdjacentRanges.
- Use static asserts to provide better diagnostics on common mistakes:
- Passing the wrong number of parameters
- Instantiating a flexclass containing an undefined type
- Check if available features are enough to replace code in LLVM (User/Uses classes)
- They are not. Need to support arrays behind the object.
- Documentation - review
- Implement
Optional
orMaybe
as a short-cut for an array with 0 or 1 element.- Then it would be possible to add an example of creating a mixin system
- Or maybe just a MultiVariant<A, B, C>, where existing objects could be just "A", "B", "C", "A B", "B C", "A C" or "A B C"
- what's the application for that?