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()>{});
}
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:
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:
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.:
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;
}