Home > other >  Initialize std::array at compile time when element's member is const. Custom to_array implement
Initialize std::array at compile time when element's member is const. Custom to_array implement

Time:01-10

At compile time I would like to create a static array from another constexpr static array.

My below attempt would work if only the members of the array element's type were not const.

new_foo_arr[idx] = Foo{
error: use of deleted function 'Foo& Foo::operator=(Foo&&)'

The error is reasonable but std::to_array can create const elements somehow. Answers I've found on initializing the array were using lambdas but that wouldn't make any difference here.

struct FooConfig {
    const std::string_view name;
};

struct Foo {
    const std::string_view name;
    const unsigned index;
    void* owner;
};

inline constexpr std::array foo_config = std::to_array<const FooConfig>(
        {{ .name = "A", },
         { .name = "B", },
         { .name = "C", },});

template<const decltype(foo_config)& arr>
consteval auto make_foo_array()
{
    std::array<Foo, arr.size()> new_foo_arr{};
    for (auto idx = 0u; idx < arr.size();   idx)
    {
        auto cfg = arr[idx];
        new_foo_arr[idx] = Foo{
                .name = std::string_view(cfg.name),
                .index = idx,
                .owner = nullptr,
        };
    }
    return new_foo_arr;
}

inline auto foo_array = make_foo_array<foo_config>();

Is there any way to cheat on the compiler? The new_foo_array is temporary anyway, so why is there so much resistance?

CodePudding user response:

There is no need to assign values after default-construct Foo array with const member, you can use index_sequence to construct Foo array directly:

template<const decltype(foo_config)& arr>
consteval auto make_foo_array()
{
  return []<std::size_t... Is>(std::index_sequence<Is...>) {
    return std::array{
      Foo{.name = std::string_view(arr[Is].name), .index = Is, .owner = nullptr}...};
  }(std::make_index_sequence<arr.size()>{});
}

Demo

CodePudding user response:

You have to add back in the copy assignment operator which is automatically deleted when an object has const member objects. Note that this might not be future proof as the default copy ctor may be removed in future c versions (> c 23). And defining the copy ctor as default prevents the class from being an aggregate. One advantage of this approach is that the objects can be put in a vector and manipulated, eg: sorting them. They behave like any normal aggregate but provide const correctness for users.

#include <string>
#include <memory>
#include <array>

struct FooConfig {
    const std::string_view name;
};

struct Foo {
    const std::string_view name;
    const unsigned index;
    void* owner;

    // This special member function must be added back in
    constexpr Foo& operator=(const Foo& arg) {
        if (this == &arg)
            return *this;
         std::destroy_at(this);
        std::construct_at(this, arg);
        return *this;
    }
};

inline constexpr std::array foo_config = std::to_array<const FooConfig>(
    { {.name = "A", },
     {.name = "B", },
     {.name = "C", }, });

template<const decltype(foo_config)& arr>
consteval auto make_foo_array()
{
    std::array<Foo, arr.size()> new_foo_arr{};
    for (auto idx = 0u; idx < arr.size();   idx)
    {
        auto cfg = arr[idx];
        new_foo_arr[idx] = Foo{
                .name = std::string_view(cfg.name),
                .index = idx,
                .owner = nullptr,
        };
    }
    return new_foo_arr;
}

inline auto foo_array = make_foo_array<foo_config>();

int main()
{
}

CodePudding user response:

Based on foo_array not being constinit / const and Foo::name and Foo::index being const, but Foo::owner not being const i'm going to assume that you want to modify Foo::owner during runtime, while ensuring that foo_array gets constant-initialized.


Given that the Foo's within new_foo_arr are not complete-const objects, we can use std::destroy_at std::construct_at to transparently replace a Foo object within the array:

godbolt

template<const decltype(foo_config)& arr>
consteval auto make_foo_array()
{
    std::array<Foo, arr.size()> new_foo_arr{};
    for (auto idx = 0u; idx < arr.size();   idx)
    {
        auto cfg = arr[idx];
        std::destroy_at(&new_foo_arr[idx]);
        std::construct_at(&new_foo_arr[idx], Foo{
                .name = std::string_view(cfg.name),
                .index = idx,
                .owner = nullptr
        });
    }
    return new_foo_arr;
}

It's not pretty, but it'll work.


Note that due tofoo_array not being const it would also be legal to replace the Foo's within foo_array at runtime, utilizing the same technique:

godbolt

int main() {
   std::destroy_at(&foo_array[0]);
   std::construct_at(&foo_array[0], Foo{
       .name = "foo",
       .index = 1,
       .owner = nullptr
   });
}

If you want Foo::name and Foo::index to be truly immutable and prevent shenanigans like the above example the easiest solution would be to make foo_array const and Foo::owner mutable, e.g.:

godbolt

struct FooConfig {
    const std::string_view name;
};

struct Foo {
    std::string_view name;
    unsigned index;
    mutable void* owner;
};

inline constexpr std::array foo_config = std::to_array<const FooConfig>(
        {{ .name = "A", },
         { .name = "B", },
         { .name = "C", },});

template<const decltype(foo_config)& arr>
consteval auto make_foo_array()
{
    std::array<Foo, arr.size()> new_foo_arr{};
    for (auto idx = 0u; idx < arr.size();   idx)
    {
        auto cfg = arr[idx];
        new_foo_arr[idx] = {
            .name = std::string_view(cfg.name),
            .index = idx,
            .owner = nullptr,
        };
    }
    return new_foo_arr;
}

inline constinit const auto foo_array = make_foo_array<foo_config>();

Now that foo_array is const it would be impossible to transparently replace a Foo member within it.

int main() {
   // this is now UB:
   std::destroy_at(&foo_array[0]);
   std::construct_at(&foo_array[0], Foo{
       .name = "foo",
       .index = 1,
       .owner = nullptr
   });

   std::cout << foo_array[0].name << std::endl;
}
  • Related