Pergunta

I'm trying to use C++ to emulate something like dynamic typing. I'm approaching the problem with inherited classes. For example, a function could be defined as

BaseClass* myFunction(int what) {
    if (what == 1) {
        return new DerivedClass1();
    } else if (what == 2) {
        return new DerivedClass2();
    }
}

The base class and each derived class would have the same members, but of different types. For example, BaseClass may have int xyz = 0 (denoting nothing), DerivedClass1 might have double xyz = 123.456, and DerivedClass2 might have bool xyz = true. Then, I could create functions that returned one type but in reality returned several different types. The problem is, when ere I try to do this, I always access the base class's version of xyz. I've tried using pointers (void* for the base, and "correct" ones for the derived classes), but then every time I want to access the member, I have to do something like *(double*)(obj->xyz) which ends up being very messy and unreadable.

Here's an outline of my code:

#include <iostream>

using std::cout;
using std::endl;

class Foo {
public:
    Foo() {};

    void* member;
};

class Bar : public Foo {

public:
    Bar() {
        member = new double(123.456); // Make member a double
    };

};

int main(int argc, char* args[]) {
    Foo* obj = new Bar;

    cout << *(double*)(obj->member);

    return 0;
};

I guess what I'm trying to ask is, is this "good" coding practice? If not, is there a different approach to functions that return multiple types or accept multiple types?

Foi útil?

Solução

That is not actually the way to do it.

There are two typical ways to implement something akin to dynamic typing in C++:

  • the Object-Oriented way: a class hierarchy and the Visitor pattern
  • the Functional-Programming way: a tagged union

The latter is rather simple using boost::variant, the former is well documented on the web. I would personally recommend boost::variant to start with.

If you want to go down the full dynamic typing road, then things get trickier. In dynamic typing, an object is generally represented as a dictionary containing both other objects and functions, and a function takes a list/dictionary of objects and returns a list/dictionary of objects. Modelling it in C++ is feasible, but it'll be wordy...


How is an object represented in a dynamically typed language ?

The more generic representation is for the language to represent an object as both a set of values (usually named) and a set of methods (named as well). A simplified representation looks like:

struct Object {
    using ObjectPtr = std::shared_ptr<Object>;
    using ObjectList = std::vector<ObjectPtr>;
    using Method = std::function<ObjectList(ObjectList const&)>;

    std::map<std::string, ObjectPtr> values;
    std::map<std::string, Method> methods;
};

If we take Python as an example, we realize we are missing a couple things:

  1. We cannot implement getattr for example, because ObjectPtr is a different type from Method
  2. This is a recursive implementation, but without the basis: we are lacking innate types (typically Bool, Integer, String, ...)

Dealing with the first issue is relatively easy, we transform our object to be able to become callable:

class Object {
public:
    using ObjectPtr = std::shared_ptr<Object>;
    using ObjectList = std::vector<ObjectPtr>;
    using Method = std::function<ObjectList(ObjectList const&)>;

    virtual ~Object() {}

    //
    // Attributes
    //
    virtual bool hasattr(std::string const& name) {
        throw std::runtime_error("hasattr not implemented");
    }

    virtual ObjectPtr getattr(std::string const&) {
        throw std::runtime_error("gettattr not implemented");
    }

    virtual void setattr(std::string const&, ObjectPtr) {
        throw std::runtime_error("settattr not implemented");
    }

    //
    // Callable
    //
    virtual ObjectList call(ObjectList const&) {
        throw std::runtime_error("call not implemented");
    }

    virtual void setcall(Method) {
        throw std::runtime_error("setcall not implemented");
    }
}; // class Object

class GenericObject: public Object {
public:
    //
    // Attributes
    //
    virtual bool hasattr(std::string const& name) override {
        return values.count(name) > 0;
    }

    virtual ObjectPtr getattr(std::string const& name) override {
        auto const it = values.find(name);
        if (it == values.end) {
            throw std::runtime_error("Unknown attribute");
        }

        return it->second;
    }

    virtual void setattr(std::string const& name, ObjectPtr object) override {
        values[name] = std::move(object);
    }

    //
    // Callable
    //
    virtual ObjectList call(ObjectList const& arguments) override {
        if (not method) { throw std::runtime_error("call not implemented"); }
        return method(arguments);
    }

    virtual void setcall(Method m) {
        method = std::move(m);
    }
private:
    std::map<std::string, ObjectPtr> values;
    Method method;
}; // class GenericObject

And dealing with the second issue requires seeding the recursion:

class BoolObject final: public Object {
public:
    static BoolObject const True = BoolObject{true};
    static BoolObject const False = BoolObject{false};

    bool value;
}; // class BoolObject

class IntegerObject final: public Object {
public:
    int value;
}; // class IntegerObject

class StringObject final: public Object {
public:
    std::string value;
}; // class StringObject

And now you need to add capabilities, such as value comparison.

Outras dicas

You can try the following design:

#include <iostream>

using std::cout;
using std::endl;

template<typename T>
class Foo {
public:
    Foo() {};
    virtual T& member() = 0;
};

class Bar : public Foo<double> {

public:
    Bar() : member_(123.456) {
    };

    virtual double& member() { return member_; }    
private:
    double member_;
};

int main(int argc, char* args[]) {
    Foo<double>* obj = new Bar;

    cout << obj->member();

    return 0;
};

But as a consequence the Foo class already needs to be specialized and isn't a container for any type anymore.
Other ways to do so, are e.g. using a boost::any in the base class

If you need a dynamic solution you should stick to using void* and size or boost::any. Also you need to pass around some type information as integer code or string so that you can decode the actual type of the content. See also property design pattern. For example, you can have a look at zeromq socket options https://github.com/zeromq/libzmq/blob/master/src/options.cpp

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top