Advertisement
alaestor

constexpr persistence dance; vectors and strings

Nov 3rd, 2024 (edited)
70
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C++ 3.64 KB | Source Code | 0 0
  1. #include <ranges> // range_value_t
  2. #include <type_traits> // invoke_result_t
  3. #include <algorithm> // copy
  4. #include <array>
  5.  
  6.  
  7. /*
  8.     Crossing the compile-time <-> runtime boundary can be an
  9.     unintuitive challenge for memory management.
  10.  
  11.     Most people want to slap `constexpr` in front of a vector or
  12.     string and be done with it; but it isn't that simple.
  13.  
  14.     std::vector's storage can't persist to runtime.
  15.     The vector merely owns dynamic / system heap memory.
  16.     All dynamic memory in compile-time contexts must be freed.
  17.     To persist the data, the dynamic regions must be flattened
  18.     to fixed-length static array
  19.     (mutable .data constinit or const .rodata constexpr)
  20.  
  21.     In an ideal world, you could have a function...
  22.     `consteval auto crystallize(constexpr std::vector<T> vec)`
  23.     however, there are two problems:
  24.     1. function parameters can't be constexpr
  25.     2. we need a constexpr size for the array template
  26.    
  27.     Point 2 is of particular importance, because we often
  28.     don't know what the size will be until we're finished
  29.     our compile-time computations.
  30.  
  31.     This example strategy uses a lambda-type range factory
  32.     so that we can perform the compile-time operations twice:
  33.     once to determine the size of array we need, and once more
  34.     to compute the values we will populate it with.
  35.  
  36.     At the time of writing, I know of no way to get a constexpr
  37.     size without duplication of effort. This also means that the
  38.     computations must be deterministic; identical invocations of
  39.     the factory must produce the same length. Having the factory
  40.     be a lambda passed by templates helps with this, a little.
  41.     It's also just generally more convenient.
  42.  
  43.     This approach should work with any sized range, however,
  44.     there can be complications due to class composition and
  45.     object lifetimes, move/copy ctors, etc.
  46.     - This is a naive example.
  47.     - Omitting template constraints for readability...
  48. */
  49.  
  50. // helps us deduce the underlying type produced by the range factory
  51. template <typename T>
  52. using typer = std::ranges::range_value_t<std::invoke_result_t<T>>;
  53.  
  54. // finds the size by doing work and discarding the result :(
  55. template <typename T>
  56. constexpr std::size_t sizer{ std::ranges::size(T{}()) };
  57.  
  58. // does the work, and copies the dynamic range to a fixed array
  59. template <typename T>
  60. consteval auto crystallizer()
  61. {
  62.     std::array<typer<T>, sizer<T>> out{};
  63.     auto&& temp{ T{}() };
  64.     std::copy(temp.begin(), temp.end(), out.begin());
  65.     return out;
  66. }
  67.  
  68. // lambda-wrapping call helper; not needed but otherwise a bit ugly.
  69. #define CRYSTALLIZE(...) crystallizer<decltype([]{ __VA_ARGS__; })>()
  70.  
  71. // Produces a vector from an array.
  72. // I'll leave the std::string equivalent as an exercise :P
  73. template <typename T_in, std::size_t T_size>
  74. std::vector<T_in> hydrate(const std::array<T_in, T_size>& in)
  75. {
  76.     std::vector<T_in> out;
  77.     out.resize(T_size);
  78.     std::copy(in.begin(), in.end(), out.begin());
  79.     return out;
  80. }
  81.  
  82.  
  83.  
  84. /// example usage
  85.  
  86.  
  87.  
  88. #include <iostream>
  89. #include <vector>
  90.  
  91. constexpr std::vector<int> func(int count)
  92. {
  93.     std::vector<int> v;
  94.     for (int i{ 0 }; i < count; ++i)
  95.         v.push_back(i);
  96.     return v;
  97. }
  98.  
  99. int main()
  100. {
  101.     static constexpr auto array1{
  102.         CRYSTALLIZE(
  103.             std::string s1 = "Hello, ";
  104.             std::string s2 = "World!\n";
  105.             return s1 + s2;
  106.         )
  107.     };
  108.     for (char c : array1)
  109.         std::cout << c;
  110.  
  111.     static constexpr auto array2{ CRYSTALLIZE( return func(3) ) };
  112.     std::cout << array2.size() << '\n';
  113.  
  114.     // since crystallizer is consteval, we can do it in-line
  115.     // and still benefit from the static storage duration
  116.     auto vec{ hydrate( CRYSTALLIZE( return func(5) ) ) };
  117.     for (auto& e : vec) // runtime vector initialized from constexpr work
  118.         std::cout << e << ", ";
  119. }
  120.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement