I have a list of entries (keyA, keyB, value) that I would like to have converted to a two-dimensional lookup table at compile time. However, due to the size of the data in question and the sparsity of its entries, it needs to be stored as an array of pointers to rows ((*table[keyA])[keyB] = value
), so that empty rows can be omitted. Storing it as a flat 2-dimensional array (table[keyA][keyB] = value
) would not be suitable.
Specifically, I want to be able to write a segment of code like this:
using my_lut_t = struct sparse_lut<10, 10, int>
#define NUM_DEFS 4
constexpr std::array<my_lut_t::entry_t, NUM_DEFS> entries = {{
{1, 1, 11},
{1, 2, 12},
{1, 3, 13},
{2, 2, 22},
}};
constinit my_lut_t table(entries);
and have the compiled binary at the other end more-or-less contain this:
section .rodata
table:
dd 0, row_1, row_2, 0, 0, 0, 0, 0, 0, 0
row_1:
dw -1, 11, 12, 13, -1, -1, -1, -1, -1, -1
row_2:
dw -1, -1, 22, -1, -1, -1, -1, -1, -1, -1
A table like this could just be assembled during runtime initialization, but that is not the purpose of this question; this question is only concerned with getting this to happen at compile time.
I've gotten as far as this implementation here:
#include <memory>
#include <array>
#include <iostream>
template<std::size_t A_COUNT, std::size_t B_COUNT, typename value_t, value_t sentinel = -1>
struct sparse_lut {
using entry_t = struct { std::size_t a; std::size_t b; value_t value; };
using table_row_t = std::array<value_t, B_COUNT>;
std::array<std::unique_ptr<table_row_t>, A_COUNT> contents = {nullptr};
template<typename Iter>
constexpr sparse_lut(Iter& definitions) {
for (const auto & entry: definitions) {
if (contents[entry.a].get() == nullptr) { // regular unique_ptr::operator== isn't constexpr
contents[entry.a] = std::make_unique<table_row_t>();
contents[entry.a]->fill(sentinel);
}
(*contents[entry.a])[entry.b] = entry.value;
}
}
value_t get(const std::size_t a, const std::size_t b) const {
if (contents[a] == nullptr) return sentinel;
return (*contents[a])[b];
}
};
using my_lut_t = struct sparse_lut<10, 10, int>;
#define NUM_DEFS 4
constexpr std::array<my_lut_t::entry_t, NUM_DEFS> entries = {{
{1, 1, 11},
{1, 2, 12},
{1, 3, 13},
{2, 2, 22},
}};
constexpr my_lut_t table(entries);
int main() {
for (std::size_t a = 0; a < 10; ++a) {
for (std::size_t b = 0; b < 10; ++b)
std::cout << table.get(a, b) << ' ';
std::cout << '\n';
}
return 0;
}
This code does work correctly to generate the table at runtime if one replaces constexpr my_lut_t table(entries);
with just const my_lut_t table(entries);
. But forcing GCC to do that work at compile time with constexpr
yields the following complaint:
In file included from /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/memory:76,
from <source>:1:
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/unique_ptr.h:1065:30: error: 'sparse_lut<10, 10, int>(entries)' is not a constant expression because it refers to a result of 'operator new'
1065 | { return unique_ptr<_Tp>(new _Tp(std::forward<_Args>(__args)...)); }
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ASM generation compiler returned: 1
In file included from /opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/memory:76,
from <source>:1:
/opt/compiler-explorer/gcc-12.2.0/include/c++/12.2.0/bits/unique_ptr.h:1065:30: error: 'sparse_lut<10, 10, int>(entries)' is not a constant expression because it refers to a result of 'operator new'
1065 | { return unique_ptr<_Tp>(new _Tp(std::forward<_Args>(__args)...)); }
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Execution build compiler returned: 1
How can I assemble a sparse lookup table from this sort of entry list, at compile time?