1

The goal is to serialize large data structures as constexpr initializations, so they will be part of the .text or .rodata segment and can be used for consteval, constexpr etc.

The data structures contain containers for cross-referencing/indexing etc. All this code will be generated, so we can compute container state inside the code generator using the same or equivalent container logic, then serialize the container state as is.

C++ does not support non-transient dynamic allocation for constexpr.

"In Kona 2019, this approach was considered too brittle, and as a result non-transient allocation was removed from the feature set." http://open-std.org/JTC1/SC22/WG21/docs/papers/2019/p0784r7.html

Therefore, the use of std:: or self-made dynamic containers is not possible.

The use of (variadic?) templates seems out of the question too, as the data structures must be usable in a runtime-dynamic manner i.e. any use of templates would require a type erasure layer on top, which seems unwieldy. See https://www.artima.com/articles/on-the-tension-between-object-oriented-and-generic-programming-in-c

The next attempt, as explored here, is trying to serialize as compound literals. See https://en.cppreference.com/w/c/language/compound_literal

Standard C++20 doesn't actually support compound literals officially, but we can rely on compiler extensions to supports this C feature inside C++. Seems to be available in all major C++ compilers.

The example below is just a very small proof of concept (or attempt at such).

Problem: The following works on Clang but not on GCC. Why? Is there a different way?

#include <memory>

template <typename K, typename V>
struct Entry{
    K key;
    V value;
};

template <typename K, typename V>
struct Bucket{
    size_t n;
    // C++ cannot initialize a flexible array in a struct, use a pointer.
    Entry<K, V> *entries; 
};

template <typename K, typename V>
struct HashMap{
    using bucket_t = Bucket<K, V>;
    using entry_t = Entry<K, V>;
    size_t n;
    // C++ cannot initialize a flexible array in a struct, use a pointer.
    bucket_t *buckets; 

    constexpr V get(K key) const {
        const size_t i = key % n;
        const auto &bucket = buckets[i];
        for (size_t j = 0; j < bucket.n; ++j) {
            if (bucket.entries[j].key == key) {
                return bucket.entries[j].value;
            }
        }
        return {};
    }
};

using MyHashMap = HashMap<int, double>;
constexpr MyHashMap map =
{
        2,
        // Because we can't initialize a flexible array, we need to employ a so-called 
        // compound literal and let it decay to a pointer. 
        (MyHashMap::bucket_t[]) 
        {
                { 
                    3, 
                    (MyHashMap::entry_t[]) // dito
                    { {0, 1.2}, {2, 2.4}, {6, 3.6} } 
                },
                { 
                    4, 
                    (MyHashMap::entry_t[]) // dito
                    { {1, 0.0}, {5, 2.0}, {11, 4.0}, {13, 1.0} } 
                }
        }
};

// Serves as proof of concept by succeeding as consteval.
consteval int proofOfConcept() {
    // Should return 76 as a constant, 
    return 10*(map.get(0) + map.get(11) + map.get(2));
}

int main() {
    return proofOfConcept();
}

On GCC this fails.

<source>: In function 'int main()':
<source>:94:26:   in 'constexpr' expansion of 'proofOfConcept()'
<source>:90:23:   in 'constexpr' expansion of 'map.HashMap<int, double>::get(0)'
<source>:94:26: error: the value of '._anon_115' is not usable in a constant expression
   94 |     return proofOfConcept();
      |            ~~~~~~~~~~~~~~^~
<source>:84:9: note: '._anon_115' was not declared 'constexpr'
   84 |         }
      |         ^

Explore it here:

https://godbolt.org/z/nMKnoY6fY

markus
  • 81
  • 6
  • The reason is that gcc thinks that your access to the content of `buckets` in `get` function is illegal and cannot be executed at compiler time. This seems to be a question about compound literals' lifetime. – 康桓瑋 May 11 '21 at 13:23
  • 2
    Your problem can be reduced to this: https://godbolt.org/z/Eb1EcW5s7. – 康桓瑋 May 11 '21 at 13:30
  • "In C, a compound literal designates an unnamed object with static or automatic storage duration. In C++, a compound literal designates a temporary object that only lives until the end of its full-expression.". see [6.28 Compound Literals](https://gcc.gnu.org/onlinedocs/gcc/Compound-Literals.html#:~:text=A%20compound%20literal%20looks%20like%20a%20cast%20of,lvalue.%20ISO%20C99%20and%20later%20support%20compound%20literals.). – 康桓瑋 May 11 '21 at 13:47
  • This is so hard and all I want is to create some constant and still dynamic data structures. – markus May 11 '21 at 14:04
  • This seems to work, but it is so complicated I get the feeling that there must be an easier way. https://godbolt.org/z/KfxKv6rrK – markus May 11 '21 at 14:05

1 Answers1

0

As 康桓瑋 pointed out, it's a lifetime issue. It seems each nested array literal needs to be defined as a symbol separately and then used in its place. This avoids the non-standard compound literal usage too.

But it adds the problem of transitive constness. We need to be able to parametrize the containing structures with constness.

So this seems to be a solution:

#include <memory>

template<bool isConst, typename T>
struct TransitiveConst { using Result = T; };

template<typename T>
struct TransitiveConst<true, T> { using Result = const T; };

template <typename K, typename V>
struct Entry{
    K key;
    V value;
};

template <typename E>
struct Bucket{
    size_t n;
    // C++ cannot initialize a flexible array in a struct, use a pointer.
    E *entries;
};

template <bool isConst, typename K, typename V>
struct HashMap{
    using entry_t = typename TransitiveConst<isConst, Entry<K, V>>::Result;
    using bucket_t = typename TransitiveConst<isConst, Bucket<entry_t>>::Result;
    size_t n;
    // C++ cannot initialize a flexible array in a struct, use a pointer.
    bucket_t *buckets;

    constexpr V get(K key) const {
        const size_t i = key % n;
        const auto &bucket = buckets[i];
        for (size_t j = 0; j < bucket.n; ++j) {
            if (bucket.entries[j].key == key) {
                return bucket.entries[j].value;
            }
        }
        return {};
    }
};

using MyHashMap = HashMap<true, const int, const double>;

constexpr MyHashMap::entry_t entry0[] = { {0, 1.2}, {2, 2.4}, {6, 3.6} };
constexpr MyHashMap::entry_t entry1[] = { {1, 0.0}, {5, 2.0}, {11, 4.0}, {13, 1.0} };
constexpr MyHashMap::bucket_t buckets[] = {
        {
            std::size(entry0),
            entry0
        },
        {
            std::size(entry1),
            entry1
        }
};

constexpr MyHashMap map =
{
        std::size(buckets),
        buckets
};

// Serves as proof of concept by succeeding as consteval.
consteval int proofOfConcept() {
    // Should return 76 as a constant,
    return (10*(map.get(0) + map.get(11) + map.get(2)));
}

int main() {
    return proofOfConcept();
}

https://godbolt.org/z/fbfTdGxE7

markus
  • 81
  • 6
  • `std::span` might simplify a little, removing the need to provide size explicitly [Demo](https://godbolt.org/z/M5T61Mx6n). – Jarod42 May 12 '21 at 09:41
  • How? I thought `std::span` has N templatized? https://en.cppreference.com/w/cpp/container/span – markus May 12 '21 at 20:20
  • > *"A span can either have a static extent, in which case the number of elements in the sequence is known at compile-time and encoded in the type, or a **dynamic extent**."* – Jarod42 May 13 '21 at 13:21
  • Interesting. I must try to understand how this can work. – markus May 14 '21 at 16:19