0

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?

AJMansfield
  • 4,039
  • 3
  • 29
  • 50
  • @fabian I just tried it, optional definitely seems like a much better way to do it than what I was doing! I'd be glad to accept that as an answer over the 'custom allocator' solution I came up with. – AJMansfield Oct 06 '22 at 21:21
  • Err, actually on second look, no that doesn't work. Each `std::optional` is still `sizeof(table_row_t)` bytes even when empty, plus metadata. – AJMansfield Oct 06 '22 at 21:25
  • If you can write a constexpr functions that can determine the used rows and their number from `entries`, you could pass that information as template parameters and therefore get rid of `unique_ptr`. – paleonix Oct 06 '22 at 21:51
  • @paleonix That _should_ be possible, I'd think? If you define a wrapper macro that expands its argument twice, once to count the number of rows to use for the size of the compile-time arena, and then the second time to populate it. – AJMansfield Dec 16 '22 at 21:14

1 Answers1

0

This can be accomplished by using a custom allocator to allocate rows from a fixed buffer, rather than dynamically as normally done by new or make_unique.

It's not clear to me whether it's possible to do this and still use unique_ptr, since it does not have custom allocator support. But using just raw table_row_t* pointers, the following achieves the desired output, at the cost of needing to manually specify the number of rows ahead of time.

#include <memory>
#include <array>
#include <iostream>
#include <cassert>

template<typename T, std::size_t N>
struct ArraySlabAllocator {
    std::array<T,N>& slab;
    std::size_t next_available = 0;
    constexpr ArraySlabAllocator(std::array<T,N>& slab): slab(slab) {}
    constexpr T* allocate() {
        if consteval {
            assert(next_available < N); // Need more rows to allocate from!
            return &slab[next_available++];
        } else {
            if (next_available < N)
                return &slab[next_available++];
            else
                return new T();
        }
    }
};

template<std::size_t A_COUNT, std::size_t B_COUNT, typename value_t, value_t sentinel=-1, std::size_t SLAB_SIZE=0>
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<table_row_t*, A_COUNT> contents = {nullptr};

    std::array<table_row_t, SLAB_SIZE> slab = {}; // we can allocate from *here* at compile time!

    template<typename Iter>
    constexpr sparse_lut(Iter& definitions) {
        auto allocator = ArraySlabAllocator(slab);
        for (const auto & entry: definitions) {
            if (contents[entry.a] == nullptr) { // regular unique_ptr::operator== isn't constexpr
                contents[entry.a] = allocator.allocate();
                contents[entry.a]->fill(sentinel);
            }
            (*contents[entry.a])[entry.b] = entry.value;
        }
    }

    constexpr 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, -1, 2>;

#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;
}

From the assembled output:

        .size   std::__array_traits<std::array<int, 10ul>*, 10ul>::_S_ref(std::array<int, 10ul>* const (&) [10], unsigned long), .-std::__array_traits<std::array<int, 10ul>*, 10ul>::_S_ref(std::array<int, 10ul>* const (&) [10], unsigned long)
        .section        .rodata
        .align 32
        .type   table, @object
        .size   table, 160
table:
        .quad   0
        .quad   table+80
        .quad   table+120
        .zero   56
        .long   -1
        .long   11
        .long   12
        .long   13
        .long   -1
        .long   -1
        .long   -1
        .long   -1
        .long   -1
        .long   -1
        .long   -1
        .long   -1
        .long   22
        .long   -1
        .long   -1
        .long   -1
        .long   -1
        .long   -1
        .long   -1
        .long   -1
AJMansfield
  • 4,039
  • 3
  • 29
  • 50