c++parametersc++17uniform-initialization

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


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


Solution

  • Since this is tagged 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

    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.