When you delete a pointer, it must be the same type that new returned (or, a pointer to a base class, if it has a virtual destructor).
You can't delete a void* pointer and expect it to know what type to destruct. All type information has been lost when the void* is assigned to.
So, you will have to explicitly type-cast the void* pointer back to its original type (just as you would have to to access the contents of the data being pointed at).
Also, you need to be sure that you follow the Rule of 3/5/0 as well, to manage the pointer property during assignments and copy/move operations.
Try this instead:
#include <iostream>
#include <string>
class MyClass {
public:
void* data;
MyClass() {
data = nullptr;
}
MyClass(const std::string &str) {
data = new std::string(str);
}
MyClass(const MyClass &src) {
data = new std::string(*static_cast<std::string*>(src.data));
}
MyClass(MyClass &&src) {
data = src.data; src.data = nullptr;
}
~MyClass() {
delete static_cast<std::string*>(data);
}
MyClass& operator=(MyClass rhs) {
MyClass tmp(std::move(rhs));
std::swap(data, tmp.data);
return *this;
}
};
int main() {
std::string str = "hi";
while (true)
{
MyClass val1(str);
MyClass val2(val1);
MyClass val3(std::move(val2));
MyClass val4;
val4 = val3;
val4 = std::move(val3);
}
}
This gets a bit more complex when you start introducing more types for the void* to point at. Then you need a way to identify what type is actually being pointed at, eg:
#include <iostream>
#include <string>
enum MyClassType { mctNull, mctString, mctInteger, ... };
class MyClass {
public:
void* data;
MyClassType dataType;
MyClass() {
dataType = mctNull;
data = nullptr;
}
MyClass(const std::string &value) {
dataType = mctString;
data = new std::string(value);
}
MyClass(int value) {
dataType = mctInteger;
data = new int(value);
}
...
MyClass(const MyClass &src) {
dataType = src.dataType;
switch (src.dataType) {
case mctNull:
data = nullptr;
break;
case mctString:
data = new std::string(*static_cast<std::string*>(src.data));
break;
case mctInteger:
data = new int(*static_cast<int*>(src.data));
break;
...
}
}
MyClass(MyClass &&src) {
dataType = src.dataType; src.dataType = mctNull;
data = src.data; src.data = nullptr;
}
~MyClass() {
switch (dataType) {
case mctString:
delete static_cast<std::string*>(data);
break;
case mctInteger:
delete static_cast<int*>(data);
break;
...
}
}
MyClass& operator=(MyClass rhs) {
MyClass tmp(std::move(rhs));
std::swap(data, tmp.data);
std::swap(dataType, tmp.dataType);
return *this;
}
};
int main() {
std::string str = "hi";
int i = 12345;
int counter;
while (true)
{
MyClass val1 = (counter++ % 2 == 0) ? MyClass(str) : MyClass(i);
MyClass val2(val1);
MyClass val3(std::move(val2));
MyClass val4;
val4 = val3;
val4 = std::move(val3);
}
}
Fortunately, modern C++ provides std::variant and std::any so you don't have to manage this stuff manually at all, eg:
#include <iostream>
#include <string>
#include <variant>
class MyClass {
public:
std::variant<std::string, int> data;
MyClass() = default;
MyClass(const std::string &value) { data = value; }
MyClass(int value) { data = value; }
...
};
int main() {
std::string str = "hi";
int i = 12345;
int counter;
while (true)
{
MyClass val1 = (counter++ % 2 == 0) ? MyClass(str) : MyClass(i);
MyClass val2(val1);
MyClass val3(std::move(val2));
MyClass val4;
val4 = val3;
val4 = std::move(val3);
}
}
You get all of the copy, move, and destruction logic for free, and it actually does the right thing for you.