Advanced Placeholder Replacement in C++: Handling Dynamic Data with Templates and Type Erasure
Tags: cpp, implementingtheworld, programming, token
Published: 2024-05-08 23:06:51

## Introduction

In many software development scenarios, especially in template engines, logging systems, or custom data processors, there's a frequent need to dynamically replace placeholders in strings with actual data. This can become complex when the data varies in type and quantity. Today, I'll show you how to elegantly handle this in C++ using templates, `std::any`, and type erasure techniques for a robust and type-safe solution.

## The Problem

Traditional methods of replacing placeholders often rely on fixed formats or limited data types. However, modern applications require more flexibility and safety, particularly when dealing with various data types and an unknown number of parameters at compile time.

## An example of the case and the result

The user wants to compose a message that have this template:

```text
Hello {{name}}, great work today! You've taken {{steps}} steps and burned {{calories}} calories. Remember: "{{quote}}"
```

We want to parse and replace with static or dynamic values, something like this:

```c++
PlaceholderManager pMgr;
  pMgr.addPlaceholder(std::make_shared<Placeholder<Person>>(
      "{{name}}", [](const Person& person) { return person.name; }));
  pMgr.addPlaceholder(std::make_shared<Placeholder<Person>>(
      "{{steps}}",
      [](const Person& person) { return std::to_string(person.steps); }));
  pMgr.addPlaceholder(std::make_shared<Placeholder<Person>>(
      "{{calories}}",
      [](const Person& person) { return std::to_string(person.calories); }));
  pMgr.addPlaceholder(
      std::make_shared<Placeholder<>>("{{quote}}", getRandomQuote));
```

We can set up a `PlaceholderManager` to hold this particular token that we want to make available to the user. And then we can use this manager to pass the input to be parsed and the list of arguments to pass to the callback that is responsible to give the actual string for that placeholder.

```c++
Person person{73, "Sheldon", 370, 1072};
const std::string res =
    pMgr.replacePlaceholders(msg, {
                                      {"{{name}}", {person}},
                                      {"{{steps}}", {person}},
                                      {"{{calories}}", {person}},
                                  });
```

And the result must be like this:

```text
Hello Sheldon, great work today! You've taken 370 steps and burned 1072 calories. Remember: "Be a fan of anything that tries to replace human contact."
```

## Solution Overview

We'll tackle this challenge by creating a system that uses:

1. **Variadic Templates**: To accept any number and type of parameters.
2. **Type Erasure with `IPlaceholder` Interface**: To manage heterogeneous types in a uniform way.
3. **`std::any`**: To store and pass parameters of different types dynamically.

## Implementation

Our system starts with an interface `IPlaceholder` that all placeholder types will implement. This interface ensures that all placeholders can be managed polymorphically.

```c++
struct IPlaceholder {
  virtual std::string resolve(const std::vector<std::any> &args) const = 0;
  virtual const std::string &getPattern() const = 0;

  virtual ~IPlaceholder() = default;
};
```

Each specific placeholder type is implemented using a template class that inherits from `IPlaceholder`. These classes can handle different types and numbers of arguments using variadic templates.
  
So here my `Placeholder` implementation, divided into small parts.
But we start with what we want to achive.
I want to be able to do something like this:

```c++
Placeholder ph("{{date}}", getSimpleDate);
```

where `{{date}}` is my placeholder, and `getSimpleDate` a function or callback that is called to replace that token.

So the first implementation of the `Placeholder` class may be this:

```c++
class Placeholder : public IPlaceholder {
 public:
  using FuncType = std::function<std::string()>;

  Placeholder(std::string p, FuncType r)
      : pattern_(std::move(p)), resolver_(std::move(r)) {}

  const std::string &getPattern() const override { return pattern_; }

  std::string resolve(const std::vector<std::any> &) const override {
    return resolver_();
  }

 private:
  std::string pattern_;
  FuncType resolver_;
};
```

In this case we export the pattern that we can use in a regex replacement, and calling the `resolve` we simply call the callback that was set in the constructor.
  
Now the problem is when we pass a callback that takes one or more arguments and returns the string to place where the token is. This will enable us to do some dynamic replacing.
  
Again, I want to be able to write something like this:

```c++
Placeholder userIdPh("{{userId}}",
                       [](const Person& person) { return std::to_string(person.id); });
```

But how to modify our `Placeholder` class?
We need to add some templates for the arguments of the `resolver`, and a `std::vector` of `std::any` for the list of his arguments.

```c++
template <typename... Args>
class Placeholder : public IPlaceholder {
 public:
  using FuncType = std::function<std::string(Args...)>;

  Placeholder(std::string p, FuncType r)
      : pattern_(std::move(p)), resolver_(std::move(r)) {}

  const std::string &getPattern() const override { return pattern_; }

  std::string resolve(const std::vector<std::any> &args) const override {
    if (args.size() != sizeof...(Args)) throw ArgCountError();
    return invoke(args, std::index_sequence_for<Args...>{});
  }

 private:
  std::string pattern_;
  FuncType resolver_;

  template <size_t... I>
  std::string invoke(std::vector<std::any> const &args,
                     std::index_sequence<I...>) const {
    return resolver_(std::any_cast<Args>(args[I])...);
  }
};
```

I want to draw your attention to how I change the `resolve` method.
The two important steps here are:

1. I want to expand the arguments that I have in a vector, to be passed to the `resolver_` function;
2. I need to select the right element and cast it to the right type.
So, to expand the arguments I start to write the `resolver_` call with the expansion of the `Args` (the variadic template argument of the class):

```c++
resolver_(args[I]...);
```

To select the index while I'm expanding the template I use the `std::index_sequence<I...>` that it will be created by the expansion of `Args` (`std::index_sequence` is a helper alias template of `std::integer_sequence` for the common case where `T` is `std::size_t`).
  
Now we add the casting to get back the right type of the argument from the `std::any`:

```c++
resolver_(std::any_cast<Args>(args[I])...);
```

So, we need to pass to this function the vector of `std::any`s and the `index_sequence`:

```c++
template <size_t... I>
std::string invoke(std::vector<std::any> const &args,
                   std::index_sequence<I...>) const {
  return resolver_(std::any_cast<Args>(args[I])...);
}
```

In the last, the `resolve` simply call the `invoke` method with the expansion of `Args` in order to create the `index_sequence`:

```c++
return invoke(args, std::index_sequence_for<Args...>{});
```

I also choose to add a check of the size of the vect of args, with the number of the variadic template `Args` using the `sizeof...` and throwing an exception `ArgCountError`:

```c++
if (args.size() != sizeof...(Args)) throw ArgCountError();
```

```c++
  std::string resolve(const std::vector<std::any> &args) const override {
    if (args.size() != sizeof...(Args)) throw ArgCountError();
    return invoke(args, std::index_sequence_for<Args...>{});
  }
```

### PlaceholderManager

Finally I introduced the `PlaceholderManager` class that keeps track of all placeholders and facilitates their replacement within strings. It matches placeholders to their data dynamically using `std::regex` and `std::map`.
I decided to escape the pattern, because like in our example above the tokens, e.g. `{{name}}`, use some character that must be escaped to be used in a regex. You can write your escape function, and let the user set his with the `setEscapingFnct` method.
The methods are:

1. `addPlaceholder(placeholder)`: The initial steps to set all the placeholders;
2. `replacePlaceholders(input, args)`: When we actually want to perform the tokens;
3. `setEscapingFnct()`.

```c++
class PlaceholderManager {
 public:
  typedef std::string(EscapingFnctTp)(const std::string &str);

  void addPlaceholder(const std::shared_ptr<IPlaceholder> &placeholder) {
    placeholders_[placeholder->getPattern()] = placeholder;
  }

  void setEscapingFnct(std::function<EscapingFnctTp> escapingFnct) {
    escapingFnct_ = escapingFnct;
  }

  // @param input The string to be modified.
  // @param args The map with vectors of arguments to pass to the functions of
  // that placeholder.
  // @return std::string The final string with all the placeholders replaced.
  // Exceptions: May throw SubstitutionError to indicate an error condition.
  std::string replacePlaceholders(
      std::string input,
      const std::map<std::string, std::vector<std::any>> &args = {}) {
    for (const auto &itPh : placeholders_)
      replaceEachPh(input, args, itPh.second);
    return input;
  }

 private:
  std::map<std::string, std::shared_ptr<IPlaceholder>> placeholders_;

  // Exceptions: May throw SubstitutionError to indicate an error condition.
  void replaceEachPh(std::string &input,
                     const std::map<std::string, std::vector<std::any>> &args,
                     const std::shared_ptr<IPlaceholder> &ph) {
    static const std::vector<std::any> empty{};

    const std::string &phStr = ph->getPattern();
    std::regex regex(escapingFnct_(phStr));
    auto it = args.find(phStr);
    const std::vector<std::any> &vArgs = it != args.end() ? it->second : empty;
    std::string fmt;

    try {
      fmt = ph->resolve(vArgs);
      try {
        input = std::regex_replace(input, regex, fmt);
      } catch (...) {
        throw SubstitutionError();
      }
    } catch (const ArgCountError &) {
    }
  }

 private:
  std::function<EscapingFnctTp> escapingFnct_{utils::escape};
};
```

### Extension of the PlaceholderManager

We can easly support our custom `PlaceholderManager` tailored to some functionality in our application. Add a predifined placeholder with corresponding function in the constructor.

```c++
class SimopPhMgr : public PlaceholderManager {
 public:
  SimopPhMgr() {
    addPlaceholder(
        std::make_shared<Placeholder<>>("{{date}}", getCurrentSimpleDate));
    addPlaceholder(
        std::make_shared<Placeholder<>>("{{iso8601}}", getCurrentIso8601));
    addPlaceholder(std::make_shared<Placeholder<SomeEvent>>(
        "{{eventName}}", [](const SomeEvent &event) { return event.name; }));
  }
};
```

## In conclusion

This advanced placeholder replacement system in C++ offers both flexibility and type safety, making it ideal for a wide range of applications where dynamic text processing is required. By leveraging modern C++ features, we can ensure robust and maintainable code.

But it is clear that there are some observations:

1. For the `Placeholder` implementation there isn't a static type check at compile time for the vector of `std::any` and the actual types of the callback;
2. For `PlaceholderManager` the `resolve` is called for all the placeholders, and only if you don't pass the args vector for that placeholder, both the resolve and the regex are skipped. But for example if you have a placeholder with a callback without arguments, the resolve and the regex are performed;
3. Possibly we can pass also the return type to the Placeholder class for the `resolver`. If the conversion of that type to string is provided, automatically convert it to std::string (for example defining a template function for conversion, and providing template specializations of that).

So, there are many things to say about this simple but very effective implementation, and there are many ways to improve it.
I was thinking that if we want to achive the first observation about type check of the vector, we can use the `tuples` to pass the arguments.
Feel free to share your thoughts about this!

Here you can find the repo with a only header placeholder.hpp
[gitlab.com/simopalacino/placeholderpp](https://gitlab.com/simopalacino/placeholderpp)

Thanks for reading.