C++ type erasure, problem, practice, solution, explanation

Srecko Kostic
15 min readNov 30, 2023

C++ type erasure, gpt generated problem, solved using type erasure based on the concept from the blog post that i’ve read, explanation of raw thoughts and final code.

Link to the code

Link to the type erasure source

https://www.modernescpp.com/index.php/type-erasure/

The problem

Problem: Mixed Data Processing

Problem Description:

You are given a sequence of operations to perform on a list of mixed data types. The data types can include integers, strings, and floating-point numbers. Each operation will specify the type of data it should be applied to and a specific action to perform. Your task is to process these operations efficiently using type erasure in C++.

Operations:

Add Data: Adds a new element of a specified type (int, string, or float) to the list.
Process Data: Applies a specified action to all elements of a particular type in the list. Actions can include:

  • For integers: Add a specified value to each integer.
  • For strings: Append a specified substring to each string.
  • For floats: Multiply each float by a specified value.

Input:

The first line contains an integer NN (1 ≤ NN ≤ 10⁵), the number of operations.
The next NN lines describe the operations. Each line starts with an operation type (“Add” or “Process”), followed by the data type (“int”, “string”, “float”), and then the operation-specific data.

Output:

After processing all operations, output the final list of elements in the order they were added, one element per line.

Example:

Input:

Output:

Explanation:

The first three operations add an integer (5), a string (“hello”), and a float (3.5) to the list.

  • The fourth operation adds 2 to the integer, making it 7.
  • The fifth operation appends “world” to the string, making it “helloworld”.
  • The sixth operation multiplies the float by 1.5, making it 5.25.

This problem will test your ability to implement type erasure in C++ to handle different data types in a single list and perform type-specific operations without resorting to explicit casting or type checking at runtime.

The solution

The thoughts

Type erasure depends on behavior, polymorphism depends on interface.

Standard library std::function requires type to be callable.

How do we require the type to behave?

A container which can perform behavioral check if it’s the correct type and which is able to process the data.

Honestly this is quite shitty problem to solve using type erasure as we do not have absolutely any benefit from anonymous type. The problem could have been solved using the following struct:

The beauty of std::function is that it’s a type that only checks if provided type is callable, the end. The type erasure is beautiful when it performs a type checking on the type it holds and asserts that it can behave in certain way. For example, it can contain any type that is:

  • processable
  • serializable
  • destructible
  • move constructible

Basically I think I can see the type erasure as a way to hold a type and assert that it’s able to do certain things. I think that is really good perspective. It is much better than confusion that I held before.

From that perspective if we have a type object_ which can contain any type that can `void process(const std::string&, const std::string&)` then we could create three different types.

  • integer_
  • string_
  • float_

Each of them would be able to process the data in a different way.

Actually each of them would receive the std::string and it would know how to process it.

The problem that is probably solvable out of the scope of this problem is how to avoid processing unrelated types. For example, it seems stupid for integer_ to receive the type string because it already knows its own type.

The thing is, type erasure is used exactly for that. We treat all processable types as the same type. However we could could still keep the generality by saying that a processable type holds a type T and a string that labels it. I do not know of a pretty way to skip a type, its actually stupid that we want to do the type check at all on the processable. Therefore that information should be held outside of it. We could store the pairs of information such as [type, processable] and for each of them, if type matches, we could process the string value. The beauty is that we can declare the processable types outside of type erasure. Now that I think of it, we are erasing all type information and performing compile time checks on the type we hold.

I wonder if it’s valid for type erased type to use different overloads of the same method. However that overload expects a concrete type.

Explanation of what I think happens:

The object contains a shared pointer to a concept.

A concept is an interface which defines the polymorphic behavior.

Actually, an object defines public interface of what’s possible to do with any type it contains. How does it achieve that?

First it contains a shared pointer to a concept, which allows runtime polymorphic behavior. At compile time the type is replaced with concrete type. So the combination of runtime polymorphism and compile time duck typing we implement the type erasure.

A public interface on the object calls the methods of the concept, which allows runtime polymorphism.

We create a concrete model of a concept, but it’s handled through the base class pointer.

The construction of an object knows a concrete type that it will hold a pointer to. The concrete type can be any class, why? Because we create a shared pointer of a model, which is our implementation, which contains a duck types T that receives an object as well.

So now a model of T contains concrete type, for example `integer_`. A model holds a type of `integer_` and just straight away calls its methods. However it calls its methods at runtime through the base class pointer.

I still don’t know which part fails at compile time if the method is not defined. I assume that if compiler sees an object duck typed replaced, aha… it then fails.

For example:

That is completely valid code, where the type T at compile time expects to have a method `process`.

At which point does it erase the type information?

We are using the type T through the object_ class. The object_ class contains explicitly defined methods and we can only call those.

Why do we need interface at all?

It looks like we do not need them because we can take the type T and hold it inside and only call the methods that we need.

Wrong. The class does not know the type that it holds. It only knows about the concept. The class through constructor takes any type and holds any type inside the model. We are utilizing the templated constructor to take any type, hold that type inside the model, and we call models methods. Model calls the types methods. The trick we use is that we always hold the same type inside the object, while the model utilizes the holding of one concrete type and calls it through duck typing. At runtime templates are what? Constructors are specialized at compile time. So now we have a class that may contain A, B, C, D within the model.

Through this we have a container which holds a model which holds any type.

ChatGPT conversation

User

I realized it requires different thinking setup to solve a problem versus trying to learn and integrate some knowledge

ChatGPT

Absolutely, solving a problem and learning or integrating new knowledge require different mindsets and approaches.

When you’re learning something new, like a concept or technique such as type erasure in programming, you’re primarily focused on understanding the theory behind it, its applications, and how it works in various contexts. This process often involves reading, experimentation, and gradual integration of the new knowledge into your existing skill set. The goal here is to build a foundation and depth of understanding.

On the other hand, problem-solving, especially in contexts like coding challenges or real-world applications, is about applying your knowledge and skills to find solutions to specific issues or tasks. This process requires you to analyze the problem, break it down into smaller parts, and then apply relevant knowledge or techniques to solve each part. It’s more about strategic thinking and application rather than learning per se.

In coding competitions like Codeforces, problems are designed to test your ability to apply programming concepts and problem-solving skills under time constraints. The challenge is to quickly understand the problem, identify the right approach (which could involve applying a specific concept like type erasure), and then code an efficient solution.

Switching between these two modes — learning and problem-solving — is a critical skill in many fields, especially in programming and other technical areas. Do you need assistance with either aspect in the context of type erasure or any other topic?

User

i find it more suitable to learn through solving even though i don’t know how to solve the problem or how to apply the concept to solve that problem but through solving I’m looking for ways and signs that show me how i can solve that problem

ChatGPT

Learning through solving problems, especially when you’re not initially sure how to apply a specific concept or technique, is an excellent approach to deepen your understanding and skills. This method, often referred to as problem-based learning, can be particularly effective in fields like programming where application and theory are closely intertwined. Here are some strategies to enhance your learning experience through problem-solving:

Start with Familiar Problems: Begin with problems that are somewhat familiar or similar to ones you’ve solved before. This allows you to build confidence and gradually introduce new concepts.

Break Down the Problem: Analyze the problem and break it down into smaller, more manageable components. This can help you identify where you might apply a new concept or technique.

Research and Learn on the Go: If you encounter a problem that seems to require a concept you’re unfamiliar with, take the time to do some quick research. Online resources, forums, and documentation can be invaluable.

Apply the Concept: Try to apply the new concept to the problem, even if you’re not entirely sure it’s the right approach. The process of trial and error is a powerful learning tool.

Reflect on the Solution: After solving the problem, reflect on what worked and what didn’t. Understanding why a particular solution was effective or why a certain approach failed is crucial for deep learning.

Iterate and Improve: Revisit the same problem after some time, and try to solve it again with your improved understanding. Alternatively, try to solve similar problems to reinforce the concept.

Engage with the Community: Participate in forums, coding communities, or study groups. Discussing with peers or mentors can provide new perspectives and insights.

Challenge Yourself: Once you feel comfortable with the concept, challenge yourself with more complex problems. This helps solidify your understanding and adaptability of the concept.

Remember, the goal is not just to find the solution, but to understand the underlying principles and how they can be applied. This approach not only enhances your problem-solving skills but also deepens your conceptual understanding.

Would you like to discuss a specific concept or problem in programming to apply this approach?

User

Problem: Mixed Data Processing

solve this problem that you gave me before

ChatGPT

To solve the “Mixed Data Processing” problem I described earlier, you will need to implement a system that can handle different types of data (integers, strings, and floating-point numbers) and apply various operations to them. Since you’re interested in using type erasure in C++, you can achieve this by using polymorphism and a container to store different types of data.

However, it’s important to note that C++ does not natively support type erasure in the same way as languages like Java. You’ll need to implement it using polymorphism (specifically, runtime polymorphism with virtual functions). This approach involves creating a base class with virtual functions for each operation and derived classes for each data type.

Here’s a basic structure for the solution:

Base Data Class and Derived Classes: Create a base class for your data types and derived classes for each specific type (int, string, float).

Container for Storing Data: Use a container (like std::vector) to store pointers to the base class.

Operations on Data: Implement functions to perform operations on the data elements.

Here’s an example solution in C++:

#include <iostream>
#include <vector>
#include <string>
#include <memory>

class BaseData {
public:
virtual void process(int val) {}
virtual void process(float val) {}
virtual void process(const std::string& val) {}
virtual void print() const = 0;
virtual ~BaseData() {}
};

class IntData : public BaseData {
int data;
public:
IntData(int val) : data(val) {}
void process(int val) override { data += val; }
void print() const override { std::cout << data << std::endl; }
};

class StringData : public BaseData {
std::string data;
public:
StringData(const std::string& val) : data(val) {}
void process(const std::string& val) override { data += val; }
void print() const override { std::cout << data << std::endl; }
};

class FloatData : public BaseData {
float data;
public:
FloatData(float val) : data(val) {}
void process(float val) override { data *= val; }
void print() const override { std::cout << data << std::endl; }
};

int main() {
std::vector<std::unique_ptr<BaseData>> dataList;

// Adding data
dataList.push_back(std::make_unique<IntData>(5));
dataList.push_back(std::make_unique<StringData>("hello"));
dataList.push_back(std::make_unique<FloatData>(3.5f));

// Processing data
for (auto& data : dataList) {
if (auto intData = dynamic_cast<IntData*>(data.get())) {
intData->process(2); // Adds 2 to integer
} else if (auto stringData = dynamic_cast<StringData*>(data.get())) {
stringData->process("world"); // Appends "world" to string
} else if (auto floatData = dynamic_cast<FloatData*>(data.get())) {
floatData->process(1.5f); // Multiplies float by 1.5
}
}

// Printing final data
for (const auto& data : dataList) {
data->print();
}

return 0;
}

In this code, we have a base class BaseData with virtual functions for different process operations. The derived classes IntData, StringData, and FloatData override these functions to implement specific behavior for each data type. The dynamic_cast is used to determine the type of data and apply the correct operation.

This solution demonstrates a basic form of type erasure, allowing different data types to be processed in a uniform manner. However, it’s important to note that this is a simple example. In a real-world application, you might want to consider more advanced techniques to avoid the use of dynamic_cast, which can be expensive in terms of performance.

User

templated c++ struct that uses that templated type to take it in constructor

ChatGPT

To create a templated C++ struct that uses its templated type in the constructor, you’ll define a struct with a template parameter. This template parameter can then be used to specify the type of data that the struct will hold and manage. This is a common practice in C++ to create generic, type-safe containers or data structures.

Here’s an example of how you can implement such a templated struct:

#include <iostream>

template <typename T>
struct MyStruct {
T data;

// Constructor that initializes 'data' with the given value
MyStruct(const T& initData) : data(initData) {}

void print() const {
std::cout << data << std::endl;
}
};

int main() {
MyStruct<int> intStruct(10);
intStruct.print(); // Outputs: 10

MyStruct<std::string> stringStruct("Hello, World!");
stringStruct.print(); // Outputs: Hello, World!

return 0;
}

In this example:

MyStruct is a templated struct with a single template parameter T.
It has a member variable data of type T.
The constructor of MyStruct takes a parameter of type T and initializes the data member with it.
A print method is added for demonstration purposes to print the contents of data.

This struct can be instantiated with different types, as shown in the main function, where MyStruct is created with int and std::string types. This kind of templated struct is useful for creating generic containers or utilities that can operate with any data type.

User

can the object know of the type returned by the child class? the issue seems to be that the type erasure shouldn’t know about any return types from the types it may hold?

ChatGPT

In the context of type erasure in C++, an object using type erasure typically does not know the concrete type of the objects it’s holding, nor the specific types returned by methods of these objects. This is because type erasure is used to abstract away the specific type information, allowing different types to be treated uniformly through a common interface.

To achieve type erasure, you usually define an interface with virtual functions that can be overridden by derived classes. However, these functions typically return a common type (like void or a base class pointer) or use templates to handle different return types in a type-safe manner.

If you need to work with different return types, there are a few approaches you can consider:

Return a Common Base Type or Wrapper: If all return types can be converted or wrapped in a common base type (like a variant, union, or any-like structure), you can use this as the return type.

Use Templates and Callbacks: You can use templated functions with callbacks or std::function to handle different return types.

Visitor Pattern: This is a design pattern that allows you to define operations on objects without changing the classes of the elements on which it operates.

Here is an example using a common wrapper type (like std::variant or a custom any-like type) for different return types:

#include <iostream>
#include <variant>
#include <memory>

class Base {
public:
virtual std::variant<int, std::string, float> getValue() const = 0;
virtual ~Base() {}
};

class IntClass : public Base {
int value;
public:
IntClass(int val) : value(val) {}
std::variant<int, std::string, float> getValue() const override {
return value;
}
};

class StringClass : public Base {
std::string value;
public:
StringClass(std::string val) : value(std::move(val)) {}
std::variant<int, std::string, float> getValue() const override {
return value;
}
};

int main() {
std::unique_ptr<Base> intObj = std::make_unique<IntClass>(10);
std::unique_ptr<Base> stringObj = std::make_unique<StringClass>("Hello");

auto intVal = intObj->getValue();
auto stringVal = stringObj->getValue();

// Use std::visit to handle different types
std::visit([](const auto& val) { std::cout << val << std::endl; }, intVal);
std::visit([](const auto& val) { std::cout << val << std::endl; }, stringVal);

return 0;
}

In this example, Base is an abstract class with a virtual function getValue that returns a std::variant. This variant can hold an int, std::string, or float, accommodating different return types from derived classes. When retrieving the value, you can use std::visit to handle the actual type stored in the variant.

This approach allows for type erasure while still providing some flexibility in the types being returned from the methods of the polymorphic objects. However, it’s important to use this pattern judiciously, as it can lead to less clear and more complex code, especially when dealing with a large number of potential types.

User

i just want a method that will print information, ok i get it, why is it that when i wrote this to you i figured out how to solve it?

ChatGPT

It’s great to hear that you figured out how to solve your problem! The phenomenon you’re describing is actually quite common and is known as “Rubber Duck Debugging” in the programming world. Let me explain why this happens:

Articulating the Problem: When you try to explain a problem to someone else (or even to an inanimate object like a rubber duck), you’re forced to articulate your thoughts more clearly. This process often helps you view the problem from a different perspective and can lead to insights that you might not have had otherwise.

Detailing the Steps: As you describe the problem, you tend to go over the steps and logic in your code more meticulously. This can help you identify any assumptions or errors in your reasoning that you might have overlooked initially.

Slowing Down: Explaining a problem to someone else forces you to slow down and consider each aspect of the problem more carefully. This can be particularly helpful in programming, where it’s easy to overlook details or get lost in complex logic.

Active Learning: The process of describing a problem and your attempts to solve it is an active learning exercise. It reinforces your understanding of the concepts involved and helps you remember solutions better in the future.

Psychological Factors: Sometimes, the mere act of discussing a problem can alleviate mental blocks, reducing the pressure and stress you might be feeling. This can clear your mind and make way for creative solutions.

So, in a way, explaining your problem here served a similar purpose to rubber duck debugging. It’s a valuable technique, and many programmers find it helpful to talk through their problems, even if it’s just to a “virtual assistant” or an actual rubber duck on their desk! If you have any more questions or need further assistance, feel free to ask.

User

i just want a method that will print information, ok i get it, why is it that when i wrote this to you i figured out how to solve it?

i realized if all i know are methods that object has, which describe behavior, i could have method that takes in std::ostream& called `out` which is responsible for streaming it’s information to std::ostream

ChatGPT

Your realization aligns perfectly with a common design pattern in C++ for handling output: the use of std::ostream& for streaming information. This approach is highly flexible and widely used, especially for scenarios like yours where objects of different types need to output their information in a uniform manner. Here’s why this method is effective and how it can be implemented:

Uniform Interface: By providing a method that takes std::ostream& in a base class and overriding it in derived classes, you create a uniform interface for outputting information. This method can be called polymorphically on objects of different types, and each type can implement its own way of displaying information.

Flexibility: Using std::ostream& allows your method to output to any stream, be it std::cout for console output, std::ostringstream for string processing, or even file streams.

Ease of Use: This approach integrates seamlessly with C++’s powerful stream operators, making it easy to chain multiple outputs together and mix different data types.

Here’s an example of how you might implement this in a type-erased setup:

#include <iostream>
#include <memory>
#include <vector>

class Base {
public:
virtual void print(std::ostream& out) const = 0;
virtual ~Base() {}
};

class DerivedInt : public Base {
int value;
public:
DerivedInt(int val) : value(val) {}
void print(std::ostream& out) const override {
out << "Integer: " << value;
}
};

class DerivedString : public Base {
std::string value;
public:
DerivedString(std::string val) : value(std::move(val)) {}
void print(std::ostream& out) const override {
out << "String: " << value;
}
};

int main() {
std::vector<std::unique_ptr<Base>> objects;
objects.push_back(std::make_unique<DerivedInt>(10));
objects.push_back(std::make_unique<DerivedString>("Hello"));

for (const auto& obj : objects) {
obj->print(std::cout);
std::cout << std::endl;
}

return 0;
}

In this code, Base is an abstract class with a virtual print method that takes a reference to std::ostream. The DerivedInt and DerivedString classes override this method to implement their own specific ways of streaming data. This design allows each object to define how it should be represented when printed, and you can easily add more derived classes with different data types and representations.

--

--

Srecko Kostic

I create content from my perspective on topics I learn. Some information may be incorrect, but I hope some find my content useful.