I'm trying to use variadic templates (for the first time really) to make a string replacement function.
Essentially, I want to make a function (we'll call it Replace) that takes a key value that is used to search up and modify a template string based on the additional parameters provided in the Replace function. The template string has placeholders using the string "%s".
My problem is, I'm not sure how to iterate through the variadic template parameters... Here is some sample code.
const std::string FindOriginal(std::string Key)
{
if(Key == "Toon")
{
return "This is example %s in my program for Mr. %s.";
}
return "";
}
std::string Replace(std::string OriginalKey) {
return "";
}
template<typename First, typename ... Strings>
std::string Replace(std::string OriginalKey, First arg, const Strings&... rest)
{
const std::string from = "%s";
std::string the_string = FindOriginal(OriginalKey);
int i = 0;
size_t start_pos = the_string.find(from);
while(start_pos != std::string::npos)
{
// Ideally here I can somehow get the parameter at index i...
the_string.replace(start_pos, from.length(), rest[i]);
start_pos = the_string.find(from, start_pos);
i ;
}
Replace(rest...);
return the_string;
}
int main()
{
std::cout << Replace("Toon", "5", "Jimmy") << std::endl;
}
CodePudding user response:
If you have access to C 17 or later, your logic is best expressed with a fold expression which can apply a function to your string for each parameter in the pack (unary right fold):
template<class T>
const std::string& ReplaceOne(std::string& originalString, const T& replacement)
{
static constexpr auto sentinel = "%s";
size_t start_pos = originalString.find(sentinel);
if(start_pos != std::string::npos)
originalString.replace(start_pos, 2, replacement);
return originalString;
}
template<class...Ts>
void Replace(std::string& originalString, const Ts&... rest)
{
(ReplaceOne(originalString, rest) , ...);
}
Called like so:
std::string original = "This is example %s in my program for Mr. %s.";
const std::string expected = "This is example 5 in my program for Mr. Jimmy.";
Replace(original, "5", "Jimmy");
assert(original == expected);
Live Demo
Essentially we are calling ReplaceOne
against our string to replace a single instance of "%s"
with the next variadic argument. If no "%s"
is found, we just return the string unchanged.
We use a unary right fold so that we work on the parameters left-to-right because order matters.
The downside to an approach like this one (and indeed the approach in your answer) is that you are calling std::string::find
for each variadic argument, which potentially searches the entire string. In the worst case, no instances of "%s"
are even in your string, and as a result, you search the entire string for each replacement. This is inefficient; O(N * M)
where N
is the length of your string, and M
is the number of arguments in your parameter pack.
We can get our time complexity down to O(N)
if we repeat calls to find
starting from the last result of find
. I'll leave that as an exercise to you for now.
(Another alternative is to store your replacement strings in a temporary container and iterate over the string in a loop like Stack Danny suggested in this answer (now deleted)
CodePudding user response:
You could:
- Search for a pattern to replace.
- Do the recursive call with the input string suffix and the rest of arguments (i.e.,
rest
). - Then concatenate the input string prefix, the current
arg
, and whatever the recursive call returned. - And return that.
I've put all this logic into another function, ReplaceImpl
, and left Replace
to do the early checks (e.g., find the original key and return if the original key is not found).
##include <iostream>
#include <string>
const std::string FindOriginal(std::string Key) {
if (Key == "Toon") {
return "This is example %s in my program for Mr. %s.";
}
return {};
}
std::string ReplaceImpl(std::string str) {
return str;
}
template <typename First, typename... Strings>
std::string ReplaceImpl(std::string str, First arg, const Strings&... rest) {
if (auto pos{ str.find("%s") }; pos != std::string::npos) {
return
str.substr(0, pos)
arg
ReplaceImpl(str.substr(pos 2), rest...);
}
return str;
}
template <typename First, typename... Strings>
std::string Replace(std::string OriginalKey, First arg, const Strings&... rest) {
if (auto the_string{ FindOriginal(OriginalKey) }; not the_string.empty()) {
return ReplaceImpl(the_string, arg, rest...);
}
return {};
}
int main() {
std::cout << Replace("Toon", "5", "Jimmy") << "\n";
}
// Outputs: This is example 5 in my program for Mr. Jimmy.
CodePudding user response:
Thanks to someone's comment regarding variadic templates working through recursive calls, I managed to change the solution to have the desired outcome. Basically we feed the parameters to a templated function (GetModifiedString) that calls the variadic template function (ReplaceValue).
#include <iostream>
#include <string>
const std::string FindOriginal(const std::string& Key)
{
if(Key == "Toon")
{
return "This is example %s in my program for Mr. %s.";
}
return "";
}
void ReplaceValue(std::string& OriginalString)
{
(void)OriginalString;
}
template<typename First, typename ... Strings>
void ReplaceValue(std::string& OriginalString, First arg, const Strings&... rest)
{
const std::string from = "%s";
size_t start_pos = OriginalString.find(from);
if(start_pos != std::string::npos)
{
OriginalString.replace(start_pos, from.length(), arg);
}
ReplaceValue(OriginalString, rest...);
}
template<typename First, typename ... Strings>
std::string GetModifiedString(const std::string& OriginalKey, First arg, const Strings&... rest)
{
std::string modified_string = FindOriginal(OriginalKey);
ReplaceValue(modified_string, arg, rest...);
return modified_string;
}
int main()
{
std::cout << GetModifiedString("Toon", "5", "Jimmyy") << std::endl;
}