Variadic templates and initializer lists

Here I'd like to show you how to utilize variadic template functions' properties and initializer lists to write better and cleaner code.

Consider we want to write a function, that accepts any number of parameters of any type and convert these parameters into a list of strings.
const auto Vec = ToStrings(1, 2.5f, "XYZ", true);
for (const auto& Value : Vec)
{
    std::cout << Value << ' ';
}
//output: 1 2.5 XYZ 1
The first thing that we need to do is to write a function that will convert any parameter into a string. String streams will be perfect for this:
template<typename T>
std::string ConvToStr(const T& mValue)
{
    std::stringstream SStream;
    SStream << mValue;
    return SStream.str();
}
To write a function that will accept any number of parameters we will use a variadic template.
template<typename T, typename ... Ts>
std::vector<std::string> ToStrings(const T& mValue, const Ts& ... mNextValues)
{
    // Initialize a vector of strings which will be our result
    std::vector<std::string> Result;
    
    // Add first element
    Result.push_back(ConvToStr(mValue));
    
    // Recursively get a vector with the rest of converted parameters 
    const auto Rest = ToStrings(mNextValues...);
    
    // Append the vector with the rest of converted parameters to the vector with first parameter
    Result.insert(Result.end(), Rest.begin(), Rest.end());
    
    // Return the final result
    return Result;
}

// The ToStrings variant with no parameters to handle end of recursion
std::vector<std::string> ToStrings()
{
    return {};
}
This is pretty straightforward solution, but we can do it better! Let's look at two c++ features: 

1. Initializer list - which can be used to construct a vector with given values: 
std::vector<int> Numbers = {1, 2, 3};
2. Variadic arguments unwrapping - which can be used to pass further multiple arguments:
  • Params... - will be translated into: Param1, Param2, ..., ParamN
  • Function(Params...); - will be translated into: Function(Param1, Param2, ..., ParamN);
  • Function(Params)... - will be translated into: Function(Param1), Function(Param2), ..., Function(ParamN)
Using these two features we can drastically simplify the previous function!
template<typename ... T>
std::vector<std::string> ToStrings(const T& ... mValues)
{
    return { ConvToStr(mValues)... };
}
And that's all! We've fit the whole function into a nice one-liner!

But there is even more we can do. Because we need ConvToString function only inside the ToStrings function we can use lambda:
template<typename ... T>
std::vector<std::string> ToStrings(const T& ... mValues)
{
    const auto ConvToStr = [](const auto& mValue)
{ std::stringstream SStream; SStream << mValue; return SStream.str(); }; return { ConvToStr(mValues)... };
}
Now we have one, solid function which does exactly what we wanted to.