Polymorphic value types and interfaces
Asked Answered
P

2

3

I have a polymorphic value type implemented like so:

class ShapeValue {
  public:
    template<class T>
    ShapeValue(const T& value) {
       obj = make_unique<holder<T>>(value);
    }
    // ... appropriate copy constructors and such

    void draw() { obj->draw(); }

  private:

    struct base {
       virtual ~base() {}
       virtual void draw() = 0;
    };

    template<class T>
    struct holder<T> : public base {
       T value;
       void draw() override { value.draw(); }
    }

    unique_ptr<base> obj;
};

If you aren't familiar with this sort of thing, here's a good talk.

Ok, that's great. But now what if I want to cast my underlying object to some other interface?

Here's my motivation. Previously, I had defined things the typical way, like so:

class Shape {
   virtual void draw() = 0;
};

and then I would define other interfaces, like:

class HasColor {
   virtual Color color() = 0;
   virtual void setColor(Color) = 0;
};

so I could define a shape as follows:

class MyShape : public Shape, public HasColor {
   void draw() override;
   Color color() override;
   void setColor(Color) override;
};

So if I have a bunch of selected shapes and I want to set their color, I could iterate over all shapes and dynamic_cast<HasColor*>. This proves to be quite convenient (my actual app isn't a drawing app, by the way, but has analogous data).

Can I do this for my polymorphic value type, in a way that my ShapeValue interface doesn't need to know about every Has interface? I could do the following, which isn't actually so bad, but not ideal:

HasColor* ShapeValue::toHasColor() { return obj->toHasColor(); }
Peashooter answered 23/7, 2019 at 16:49 Comment(2)
Is your concern binary bloat, hand written code bloat, generated code bloat, having a central list, or what? "Bloat" remains vague to me. Do you understand what the traditional C++ virtual tables generate behind the scenes, bloat-wise?Arcboutant
@Yakk-AdamNevraumont Yes I understand v-tables. I've attempted to clarify the question.Peashooter
P
2

A solution (tested) is to have a base class for the interfaces:

class AnyInterface {
   virtual ~AnyInterface() {} // make it polymorphic
};

struct HasColor : public AnyInterface {
   // ... same stuff
};

So then we have the following:

vector<AnyInterface*> ShapeValue::getInterfaces() { return _obj->getInterfaces(); }

Could then define a helper to grab the interface we want:

template<class I>
I* hasInterface(Shape& shape) {
   for(auto interface : shape.getInterfaces()) {
       if(auto p = dynamic_cast<I*>(interface)) {
           return p;
       }
   }
   return nullptr;
}

This way ShapeValue does not need to know about all the interface types.

Peashooter answered 23/7, 2019 at 17:20 Comment(2)
This doesn't work, because the interfaces aren't actually used in his value polymorphism system.Arcboutant
@Yakk-AdamNevraumont I wrote a unit test and it works.Peashooter
F
3

The accepted answer seems likely a viable solution though I haven't tested it and it does seem to fallback to reference semantics. A motivating factor however for polymorphic value types is instead value semantics.

What follows is a description of a more value semantic oriented alternative solution where ShapeValue doesn't need to know about all the interface types, albeit external user-definable free functions sort of do instead.

As I've been using polymorphic value types, I've preferred to recognize two categories of functionality of those values:

  1. Functionality required of all eligible value types. I.e. the functionality enforced by the virtual methods of this base polymorphic concept class.
  2. Optional/extended functionality which some, none, or all eligible value types may provide.

It seems like your question is more about how to deal this second category (than the first).

For this second category, I've borrowed on the implementation of std::any's type member function and std::any's non-member any_cast template functions. With these two functional concepts, the set of value types, which implement some optional extended functionality, is open (like namespaces are open to additions contrary to classes) and your ShapeValue's interface doesn't need to know about every optional extension. As an added bonus, no extended functionality needs to be implemented using type polymorphism - i.e. the value types eligible for use with ShapeValue construction, don't have to have any kind of inheritance relationship or virtual functions.

Here's an example of pseudo code extending the question's code for this:

class ShapeValue {
  public:
    template<class T>
    ShapeValue(const T& value) {
       obj = make_unique<holder<T>>(value);
    }
    // ... appropriate copy constructors and such

    ShapeValue& operator=(const ShapeValue& newValue) {
        obj = newValue.obj? newValue.obj->clone(): nullptr;
        return *this;
    }

    const std::type_info& type() const noexcept {
        return obj? obj->type_(): typeid(void);
    }

    void draw() { obj->draw(); }

    template <typename T>
    friend auto type_cast(const ShapeValue* value) noexcept {
        if (!value || value->type() != typeid(std::remove_pointer_t<T>))
            return static_cast<T>(nullptr);
        return static_cast<T>(value->obj->data_());
    }

  private:

    struct base {
       virtual ~base() = default;
       virtual void draw() = 0;
       virtual std::unique_ptr<base> clone_() const = 0;
       virtual const std::type_info& type_() const noexcept = 0;
       virtual const void* data_() const noexcept = 0;
    };

    template<class T>
    struct holder final: base {
       T value;
       void draw() override { value.draw(); }
       std::unique_ptr<base> clone_() const override {
           return std::make_unique<holder>(value);
       }
       const std::type_info& type_() const noexcept override { return typeid(T); }
       const void* data_() const noexcept override { return &value; }
    };

    unique_ptr<base> obj;
};

template <typename T>
inline auto type_cast(const ShapeValue& value)
{
    auto tmp = type_cast<std::add_pointer_t<std::add_const_t<T>>>(&value);
    if (tmp == nullptr)
        throw std::bad_cast();
    return *tmp;
}

struct Square {
    int side_;
    Color color_;
    void draw();
    Color color() { return color_; }
    void setColor(Color value) { color_ = value; }
};

Color color(const ShapeValue& value)
{
    if (value.type() == typeid(Square)) {
        return type_cast<Square>(value).color();
    }
    throw std::invalid_argument("color not supported for value's type");
}

void setColor(ShapeValue& value, Color newColor)
{
    if (value.type() == typeid(Square)) {
        auto square = type_cast<Square>(value);
        square.setColor(newColor);
        value = square;
        return;
    }
    throw std::invalid_argument("setColor not supported for value's type");
}

For a more elaborate, compilable, tested, and typeid/std::type_info-free example, one can take a look at the source code for the Joint polymorphic value type I just finished that provides an interface to value types for constraining the movements of one or more bodies. I wouldn't say it's perfect, but it's also more value semantics oriented like the example above that I've included in this answer.

Fiber answered 21/10, 2020 at 19:58 Comment(2)
Doesn't this require color and setColor to know about all shapes that have colors?Peashooter
@Peashooter If I understand correctly that you mean the implementations for the color(const ShapeValue&) and setColor(ShapeValue&) non-member functions, then yes. As I'd pointed out, "ShapeValue doesn't need to know about all the interface types, albeit external user-definable free functions sort of do instead". That may not be as elegant to some as say using std::visit on a std::variant, but it's not reference semantics, it's the same aggregation of knowledge as say visiting a std::any, and std::any is basically just the totally open standard library polymorphic value type.Fiber
P
2

A solution (tested) is to have a base class for the interfaces:

class AnyInterface {
   virtual ~AnyInterface() {} // make it polymorphic
};

struct HasColor : public AnyInterface {
   // ... same stuff
};

So then we have the following:

vector<AnyInterface*> ShapeValue::getInterfaces() { return _obj->getInterfaces(); }

Could then define a helper to grab the interface we want:

template<class I>
I* hasInterface(Shape& shape) {
   for(auto interface : shape.getInterfaces()) {
       if(auto p = dynamic_cast<I*>(interface)) {
           return p;
       }
   }
   return nullptr;
}

This way ShapeValue does not need to know about all the interface types.

Peashooter answered 23/7, 2019 at 17:20 Comment(2)
This doesn't work, because the interfaces aren't actually used in his value polymorphism system.Arcboutant
@Yakk-AdamNevraumont I wrote a unit test and it works.Peashooter

© 2022 - 2024 — McMap. All rights reserved.