-3

I want to create a universal template function that replaces all occurrences of a search string in a given string object with the replace text lazy-evaluated, possibly by calling a functor.

The main differences from the function I need, to the usual functions given in answers to questions like How to replace all occurrences of a character in string? are:

  • The string to be manipulated, as well as the search string, needs not to be of a concrete type like std::string. It's sufficient to be string-like.
  • The new text parameter, used to replace the search string, may be a value or a functor that will be invoked to deliver a value
  • The new text is only evaluated, when the search string is found
  • The new text is only evaluated once, even when it is used multiple times (in case it occurs more than once in the string to be manipulated)
  • The new text can have a type different from the type of the search string or the string to be manipulated. It is only required to be convertible into a std::string_view<>, compatible to the string to be manipulated. This means that char*, std::string_view, std::string, char*(*)() and std::function<string()> are all fine.

Here is what I want to write, that makes this design very universal and very different from the answers in similar but simpler questions:

  • The function should accept all possible kinds of text types (char*, std::string_view, std::string, char*(*)(), std::function<string()>, ...) for the new (replace) text. Anything that evaluates to something that is assignable to a std::basic_string_view<> should also work as an argument for the new replacement text. Especially text coming from a lazy-evaluated functor.

  • The function should be able to manipulate text in all std::basic_string-like string classes, especially std::string and std::wstring. And usable also on other classes that provide the required functions in their interface.

  • The function must handle the case, where the new text comes from a call to a function returning a temporary object (std::string for example). So it needs to extend the lifetime of this temporary (or copy the value, but this seems inefficient and should be avoided)

  • The new (replace) string parameter should only be evaluated once, even when several replacements occur (at least in case the replace parameter is a functor).

  • The function should be optimized also for the case, where no replacement occurs. That is, it should not create expensive temporary objects (such as a temporary std::string), if they are not subsequently used. (Reason: My use case consists of a very large number of calls to the wanted replace function, with only a few actual replacements.)

  • I want an all-in-one single function.

  • I want the code to work with VS2022 and C++20, but solutions for other compilers and newer C++ revisions may help others (so I do not add the VS2022 tag)

The function could have a prototype like this:

auto& replace_all(auto& string, auto search, auto replace)

Then here is a code example to test the function:

std::string t = "hello world";
replace_all(t, "hello", []() { return "Hello"; });
replace_all(t, std::string("Hello"), "HELLO");
replace_all(t, "world", std::string("World"));
replace_all(t, "World", []() { return std::string("WORLD"); });
replace_all(t, "WORLD", []() { return "sunshine"; });
replace_all(t, std::string("HELLO"), []() { return "good day"; });
replace_all(t, "&", []() -> std::string { return (const char*)0; });  //never evaluated!
assert(t == "good day sunshine");

Note:

I moved my own solution (which isn't meeting all requirements) with improvements from Pepijn Kramer and Igor Tandetnik to an answer, but I am seeking improvements, consisting only of one function, if possible.

16
  • Are you asking how to fix the compiler error that you describe at the end? Or are you asking something else? Commented Aug 21 at 16:43
  • @Drew I need a working solution. So if someone could fix the compiler error in the second function (replace_all1) and this will give me the functionality I need, I would be very happy. But if the approach of the second function (replace_all1) is nonsense, then improvements on the first function should help. Commented Aug 21 at 16:49
  • 1
    In general just using std::string_view as "string like" works for me. And a question why would std::regex_replace not work for you? Commented Aug 21 at 17:18
  • @Pepijn My trial with std::string_view led to the problem, that using a temporary string object for the replace text parameter (replaceExpr or replace in my functions) ended in dangling references in this string_view. Commented Aug 21 at 17:24
  • 1
    Put the common code into a lambda or a helper function, call it from both branches of the if constexpr Commented Aug 21 at 17:49

1 Answer 1

1

Here is my solution of a quite universal text replace function, supporting all but one of the requirements of the question.

It works, but I'm not happy with it, as it consists of two functions. And that makes one requirement not met.

With suggenstions from Pepijn Kramer and Igor Tandetnik, I came up with my current solution:

template <typename T>
using char_type_t = typename std::conditional_t<
  std::is_same_v<std::decay_t<T>, std::string>, 
    char,
    typename std::conditional_t<std::is_same_v<std::decay_t<T>, std::wstring>,
      wchar_t,
      typename std::remove_pointer_t<std::decay_t<T>
    >
  > 
>;

auto& replace_all(auto& string, auto search, auto replace)
{
  using string_type = decltype(string);
  using char_type = char_type_t<string_type>;
  using string_view = std::basic_string_view<char_type, std::char_traits< char_type>>;
  size_t pos = string.find(search);
  if(pos == std::basic_string<char_type>::npos)
    return string;
  string_view search_{search};
  auto do_replace = [&](string_view replaceStr)
  {
    do
    {
      string.replace(pos, search_.size(), replaceStr);
      pos += std::size(replaceStr);
    } while((pos = string.find(search, pos)) != std::basic_string<char>::npos);
  };
  if constexpr(std::is_invocable_v<decltype(replace)>)
  {
    const auto& replace_ret = replace();
    do_replace(replace_ret);
  }
  else
    do_replace(replace);
  return string;
}

Details:
I use std::string_view for the text to search and for the text to replace, to handle all kinds of input text types. And for the replace text coming from a functor call, it uses a const lvalue reference in order to extend the lifetime of the result of the call (const auto& replace_ret = replace();).

This seems to work with all types I required for the replace parameter. That is, for the new text, I can use char*, std::string_view, std::string, a function char*(*)() and a functor std::function<string()> (or the wchar_t equivalents, as I'm on Windows).

So this solution addresses all but one requirements. I mean, it consists of two functions (the main replace_all function and the helper function char_type_t<> used to evaluate the underlying character type). So it fails to meet my requirement to be just one function.

Sign up to request clarification or add additional context in comments.

1 Comment

This answer contains my own solution with improvements by Pepijn Kramer and Igor Tandetnik so far. It's here as it is the solution I would need to choose, if no one else comes with an answer meeting all requirements.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.