pbuzz007
pbuzz007

Reputation: 947

Classes member tied to enum

I'm facing maybe a non-problem with some C++ class design, mostly because I need to integrate a database into my system. I've got a base class that has some child classes. I find it redundant to have a member called type which is an enum which described the type of child it is. Example

enum FooTypes {
    kFooTypeGeneric,
    kFooTypeA,
    kFooTypeB
};

class Foo {
public:
    Foo(FooTypes type):type(type){}
    FooTypes type;
};

class FooA : public Foo {
public:
    FooA():Foo(kFooTypeA){}
};

class FooB : public Foo {
public:
    FooB() :Foo(kFooTypeB) {}
};

The reason that I feel forced to maintain an enum is because owners of Foos want to store what they have created into a database table. If the system reboots, they should be able to look into their own table and say, "Oh yeah, I have a FooA that I need to initialize", and that can only really be done by setting a column called "FooType" to 1 in this case.

I'm just wondering if this way of giving child classes a type that is part of an enum that they then must know about is bad design. It seems redundant among other things.

Upvotes: 3

Views: 138

Answers (2)

Ted Lyngmo
Ted Lyngmo

Reputation: 117298

You could use polymorphism and use overridden streaming functions for the derived classes. You'd then need a factory function to create different derived object depending on what you read from the database.

Here's a small example where the database is an std::istringstream holding what has previously been saved.

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

// An abstract base class for all entities you can store in the database
struct Foo {
    virtual ~Foo() = default;
    virtual void serialize(std::ostream&) const = 0; // must be overridden
    virtual void deserialize(std::istream&) = 0;     // must be overridden
};

// streaming proxies, calling the overridden serialize/deserialize member functions
std::ostream& operator<<(std::ostream& os, const Foo& f) {
    f.serialize(os);
    return os;
}

std::istream& operator>>(std::istream& is, Foo& f) {
    f.deserialize(is);
    return is;
}
//--------------------------------------------------------------------------------------
class FooA : public Foo {
public:
    void serialize(std::ostream& os) const override {
        // serializing by streaming its name and its properties
        os << "FooA\n" << a << ' ' << b << ' ' << c << '\n';
    }

    void deserialize(std::istream& is) override {
        // deserializing by reading its properties
        if(std::string line; std::getline(is, line)) {
            std::istringstream iss(line);
            if(not (iss >> a >> b >> c)) is.setstate(std::ios::failbit);
        }
    }

private:
    int a, b, c;
};

class FooB : public Foo {
public:
    void serialize(std::ostream& os) const override {
        os << "FooB\n" << str << '\n';
    }

    void deserialize(std::istream& is) override {
        std::getline(is, str);
    }

private:
    std::string str;
};
//--------------------------------------------------------------------------------------
// A factory function to create derived objects from a string by looking it up in a map
// and calling the mapped functor.
//--------------------------------------------------------------------------------------
std::unique_ptr<Foo> make_foo(const std::string& type) {
    static const std::unordered_map<std::string, std::unique_ptr<Foo>(*)()> fm = {
        {"FooA", []() -> std::unique_ptr<Foo> { return std::make_unique<FooA>(); }},
        {"FooB", []() -> std::unique_ptr<Foo> { return std::make_unique<FooB>(); }},
    };
    if(auto it = fm.find(type); it != fm.end()) return it->second(); // call the functor
    throw std::runtime_error(type + ": unknown type");    
}
//--------------------------------------------------------------------------------------
// Deserialize all Foos from a stream
//--------------------------------------------------------------------------------------
std::vector<std::unique_ptr<Foo>> read_foos(std::istream& is) {
    std::vector<std::unique_ptr<Foo>> entities;

    std::string type;
    while(std::getline(is, type)) {     // type is for example "FooA" here
        // Call make_foo(type), put the result in the vector and
        // stream directly to the added element (C++17 or later required)

        if(not (is >> *entities.emplace_back(make_foo(type)))) {
            throw std::runtime_error(type + ": deserializing failure");
        }
    }
    return entities;
}
//--------------------------------------------------------------------------------------
int main() {
    std::istringstream db(
        "FooA\n"
        "1 2 3\n"
        "FooB\n"
        "Hello world\n"
    );

    auto entities = read_foos(db);

    std::cout << "serialize what we got:\n";
    for(auto& fooptr : entities) {
        std::cout << *fooptr;
    }
}

Output:

serialize what we got:
FooA
1 2 3
FooB
Hello world

Upvotes: 1

Asteroids With Wings
Asteroids With Wings

Reputation: 17454

This is fine. Generally you want to avoid the need to "know" a type in a polymorphic hierarchy ‐ if you're fine with the performance of virtual dispatch, it should be all you need in a good design.

But sometimes you do need a strong mapping (say, for passing to some external resource), and having an enum identify the actual "type" of an instance saves you having a string of dynamic_cast to do the same job.

You could have the best of both worlds, and make a virtual function that returns the right enum for the class. But then frankly you're forcing a loss of performance for nothing.

Upvotes: 0

Related Questions