I would like to simulate python sets in C++.
According to the faq, an idiomatic way to do this would be to use an std::vector
which stores pointers (better if smart pointers) to a handle class which is then overloaded by the specific types which we would like to store in the simulated set.
This is my attempt:
// experimenting with vectors of heterogeneous elements (like sets in python)
#include <iostream>
#include <memory>
#include <string>
#include <vector>
// Handle can wrap an int, a double, or a string.
class Handle {
public:
Handle();
virtual int val();
virtual double val();
virtual std::string val();
};
class Int : public Handle {
public:
Int(const int n) : value(n) {};
int val() override { return value; };
private:
int value;
};
class Double : public Handle {
public:
Double(const double n) : value(n) {};
double val() override { return value; };
private:
double value;
};
class String : public Handle {
public:
String(const std::string& n) : value(n) {};
std::string val() override { return value; };
private:
std::string value;
};
int main() {
// v simulates a set with 12 elements of types int, double, or string.
std::vector<std::shared_ptr<Handle>> v(12);
for (int i = 0; i < v.size(); i += 3) {
v[i] = std::shared_ptr<Int>(new Int(i));
v[i + 1] = std::shared_ptr<Double>(new Double(i + 1.));
v[i + 2] = std::shared_ptr<String>(new String(std::to_string(i + 2)));
}
for (auto& it : v) {
std::cout << it->val() << std::endl;
}
}
This approach would be preferable to declaring v
as an std::vector<Handle*>
, because using smart pointers would allow me to forget about deleting manually the pointers, while at the same time achieving runtime polymorphism.
The code above however does not compile, because we are not allowed to overload a function (val()
in this case) only on the return types.
Is there a way to fix my code so that I can simulate a set with different types? Is there a better, idiomatic way to do it?
Here's how I changed my code after n. 'pronouns' m.'s answer. The code seems to work, and I am happy with it as an exercise in using inheritance and smart pointers (even though it's not the best solution for the specific problem at hand, see Maxim Egorushkin's answer for a much better approach).
// experimenting with vectors of heterogeneous elements (like sets in python)
#include <iostream>
#include <memory>
#include <string>
#include <vector>
// Handle can wrap an int, a double, or a string.
class Handle {
public:
Handle() {};
virtual void print(std::ostream&) = 0;
};
class Int : public Handle {
public:
Int(const int n) : value(n) {};
void print(std::ostream& s) override { s << value << std::endl; };
private:
int value;
};
class Double : public Handle {
public:
Double(const double n) : value(n) {};
void print(std::ostream& s) override { s << value << std::endl; };
private:
double value;
};
class String : public Handle {
public:
String(const std::string& n) : value(n) {};
void print(std::ostream& s) override { s << value << std::endl; };
private:
std::string value;
};
int main() {
std::vector<std::shared_ptr<Handle>> v(12);
for (size_t i = 0; i < v.size(); i += 3) {
v[i] = std::shared_ptr<Int>(new Int(i));
v[i + 1] = std::shared_ptr<Double>(new Double(i + 1.));
v[i + 2] = std::shared_ptr<String>(new String(std::to_string(i + 2)));
}
for (const auto& it : v) {
it->print(std::cout);
}
}
C++ is a statically typed language. Every expression has a statically-determined type. This means you, tge compiler, and everybody else are able to determine the type from the program text alone, without trying to execute any code.
(An object denoted by tge expression may also have a dynamic type, determined at run time, but this is not what we are concerned with). A
Let's look at this expression.
it->val()
What is its type?
The only possible answer is std::string
. Indeed, the type of it
is std::shared_ptr<Handle>
, and Handle::val
is specified to return std::string
. The fact Handle
has derived types plays no role. Whatever the dynamic type of the pointed-to object is, the static type of it->val()
is std::string
, and if something has static type of std::string
, it cannot have dynamic type of say double
. This is the reason why you cannot override val
this way.
So what should we do if we want to print different things depending on the dynamic type of the object?
The standard OOP solution is to have a method that prints, as oposed to a method that returns a thing to be printed. In C++ terms that would be a virtual function, e.g.
virtual void print() = 0;
overriden in each derived class.
Of course the real life isn't that simple. What if we need to print to an arbitrary stream? We need to pass a stream as an argument to print
. Perhaps we need to.pass some more stuff. Maybe we want to specify a printed field width, or precision, or whatever. What if we want to do a bunch of other stuff besides printing? Do we need a separate method for each thing we want to do? OOP studies answers to these and more questions, but I cannot answer them in a single SO post :(