I have a vector of 2N lines where the second half (N lines) is basically the same as the first half but with a single character changed, e.g.:
std::vector<std::string> tests{
// First half of lines with '=' as separator between key and value
"key=value",
...
// Second half of lines with ' ' as separator between key and value
"key value",
...
};
Is there a way to parameterize the separator (i.e., =
or
) to avoid repeating the lines during the initialization, using the uniform initialization construct? I wonder if there's a better way than creating it with a for
loop.
By reading from documentation, it seems not to be possible.
Thanks
Since this is tagged c++17 and you said the strings are known at compile-time, it is technically possible to perform uniform initialization by leveraging a variadic function-template and unpacking the parameters twice, and this would produce the modified string at compile-time.
The idea, in the simplest form, is to do this:
template <typename...Strings>
auto make_string_vector(const Strings&...strings) -> std::vector<std::string>
{
// Unpacks the strings twice; first unmodified, second with '=' swapped with ' '
return std::vector<std::string>{{
strings..., to_space_string(strings)...
}};
};
Where to_space_string
is a class that returns a string-like object, done at compile-time.
To do this, we need to make a simple holder that acts like a string is convertible to std::string_view
at compile time. This is to ensure that the string we modify has its own separate lifetime and does not dangle:
// A holder for the data so that we can convert it to a 'std::string' type
template <std::size_t N>
struct static_string {
char data[N];
constexpr operator std::string_view() const noexcept {
return std::string_view{data, N};
}
};
Then all we need is the function that takes a compile-time string (array of char
s), copies it into the static_string<N>
object, and returns it:
// std::string_view used so that we can do this constexpr
template <std::size_t N>
constexpr auto to_space_string(const char(&string)[N]) -> static_string<N>
{
auto storage = static_string<N>{};
std::transform(&string[0], &string[N], &storage.data[0], [](char c){
if (c == '=') { return ' '; }
return c;
});
return storage;
}
The last needed tweak would be for the initializer list to be a sequence of std::string
objects, which we can do with static_cast
s:
template <typename...Strings>
auto make_string_vector(const Strings&...strings) -> std::vector<std::string>
{
return std::vector<std::string>{{
static_cast<std::string>(strings)...,
static_cast<std::string>(to_space_string(strings))...
}};
};
With this, code like:
auto vec = make_string_view("hello=world", "goodbye=world");
will produce a vector containing
hello=world
goodbye=world
hello world
goodbye world
Note:
If we didn't use a static_string
or some equivalent and instead used string_view
directly, the string would dangle. For example:
template <std::size_t N>
constexpr auto to_space_string(const char(&string)[N]) -> std::string_view
{
char storage[N];
std::transform(&string[0], &string[N], &storage[0], [](char c){
if (c == '=') { return ' '; }
return c;
});
// dangles after being returned!
return std::string_view{&storage[0], N};
}
In the above case, we return a reference to temporary storage storage[N]
, thus causing a dangling pointer (UB). The static_string
creates an object first whose lifetime is passed into the caller (make_string_vector
) and then gets converted to a std::string
.