c++ 17, is it possible to parameterize uniform initialization of vector?

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 chars), 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_casts:

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

Live Example


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.


If only one = per line is allowed then following can work

std::vector<std::string> init_from_key_eq_val(std::initializer_list<const char *> strings)
    {
        std::vector<std::string> result;
        result.reserve(2 * strings.size());
        copy(strings.begin(), strings.end(), back_inserter(result));
        transform(strings.begin(), strings.end(), back_inserter(result),
                  [](const char* str)
                  {
                      std::string str_copy(str);
                      size_t eq_pos = str_copy.find('=');
                      str_copy[eq_pos] = ' ';
                      return str_copy;
                  });
        return result;
    }
    
    int main()
    {
        std::vector<std::string> vec = init_from_key_eq_val({"key=value"});
    }

From the performance point of view, it is no better than doing it with a loop though