Reputation: 53236
I have an array of strings that must be allocated once and their underlying c_str
must remain valid for entire duration of the program.
There's some API that provides info about some arbitrary data types. Could look like this:
// Defined outside my code
#define NUMBER_OF_TYPES 23
const char* getTypeSuffix(int index);
The getTypeSuffix
is not constexpr
, so this must work at least partially in runtime.
The interface that I must provide:
// Returned pointer must statically allocated (not on stack, not malloc)
const char* getReadableTypeName(int type);
Now my array should have following type:
std::string typeNames[NUMBER_OF_TYPES];
For my purposes, it will be initialized within a wrapper class, right in the constructor:
class MyNames
{
MyNames()
{
for (int i = 0; i < NUMBER_OF_TYPES; ++i)
{
names[i] = std::string("Type ") + getTypeSuffix(i);
}
}
const char* operator[](int type) { return _names[(int)type].c_str(); }
private:
std::string _names[NUMBER_OF_TYPES];
};
This is then used in an singleton-ish kind of way, for example:
const char* getReadableTypeName(int type)
{
static MyNames names;
return names[type];
}
Now what I want to improve is that I can see that the for loop in the constructor could be replaced as such:
MyNames() : _names{std::string("Type ") + getTypeSuffix(0), std::string("Type ") + getTypeSuffix(1), ... , std::string("Type ") + getTypeSuffix(NUMBER_OF_TYPES-1)}
{}
Obviously a pseudocode, but you get the point - the array can be initialized directly, leaving the constructor without body, which is neat. It also means that the array member _names
can be const
, further enforcing the correct usage of this helper class.
I'm quite sure there would be many other uses to filling an array by expressions in compile time, instead of having loop. I would even suspect that this is something that happens anyway during 03
.
Is there a way to write a C++11 style array initializer list that has flexible length and is defined by an expression? Another simple example would be:
constexpr int numberCount = 10;
std::string numbers[] = {std::to_string(1), std::to_string(2), ... , std::to_string(numberCount)};
Again, an expression instead of a loop.
I'm not asking this question because I was trying to drastically improve performance, but because I want to learn about new, neat, features of C++14 and later.
Upvotes: 2
Views: 351
Reputation: 23701
Since you ache to use new features, let's use range-v3
(soon-to-be the ranges
library in C++2a) to write some really short code:
const char* getReadableTypeName(int type)
{
static const std::vector<std::string> names =
view::ints(0, 23) | view::transform([](int i) {
return "Type " + std::to_string(i);
});
return names[type].c_str();
}
Upvotes: 1
Reputation: 303357
You can defer to an initialization function:
std::array<std::string, NUMBER_OF_TYPES> initializeNames()
{
std::array<std::string, NUMBER_OF_TYPES> names;
for (int i = 0; i < NUMBER_OF_TYPES; ++i) {
names[i] = std::string("Type ") + getTypeSuffix(i);
}
return names;
}
const char* getReadableTypeName(int type)
{
static auto const names = initializeNames();
return names[type].c_str();
}
which can be an immediately invoked lambda:
static auto const names = []{
std::array<std::string, NUMBER_OF_TYPES> names;
// ...
return names;
}();
or do you really need the array
requirement? We're making strings anyway so I don't understand, then you can just use range-v3:
char const* getReadableTypeName(int type) {
static auto const names =
view::iota(0, NUMBER_OF_TYPES)
| view::transform([](int i){ return "Type "s + getTypeSuffix(i); })
| ranges::to<std::vector>();
return names[type].c_str():
}
Upvotes: 3
Reputation: 40891
You can use std::make_integer_sequence
and a delegating constructor in C++14 (Implementations of std::make_integer_sequence
exist in C++11, so this is not really C++14 specific) to get a template parameter pack of integers
#include <string>
#include <utility>
#define NUMBER_OF_TYPES 23
const char* getTypeSuffix(int index);
class MyNames
{
MyNames() : MyNames(std::make_integer_sequence<int, NUMBER_OF_TYPES>{}) {}
template<int... Indices>
MyNames(std::integer_sequence<int, Indices...>) : _names{ (std::string("Type ") + getTypeSuffix(Indices))... } {}
const char* operator[](int type) { return _names[(int)type].c_str(); }
private:
const std::string _names[NUMBER_OF_TYPES];
};
This means that no strings are being default constructed.
Upvotes: 2
Reputation: 217810
instead of C-array use std::array
, then you might write your function to return that std::array
and your member can then be const
:
std::array<std::string, NUMBER_OF_TYPES> build_names()
{
std::array<std::string, NUMBER_OF_TYPES> names;
for (int i = 0; i < NUMBER_OF_TYPES; ++i)
{
names[i] = std::string("Type ") + getTypeSuffix(i);
}
return names;
}
class MyNames
{
MyNames() : _names(build_names()) {}
const char* operator[](int type) const { return _names[(int)type].c_str(); }
private:
const std::array<std::string, NUMBER_OF_TYPES> _names;
};
Now you have std::array
, you might use variadic template instead of loop, something like (std::index_sequence
stuff is C++14, but can be implemented in C++11):
template <std::size_t ... Is>
std::array<std::string, sizeof...(Is)> build_names(std::index_sequence<Is...>)
{
return {{ std::string("Type ") + getTypeSuffix(i) }};
}
and then call it:
MyNames() : _names(build_names(std::make_index_sequence<NUMBER_OF_TYPES>())) {}
Upvotes: 6