1

I have a base class Object:

struct Object{
};

and n (in this case 2) classes that inherit from this

struct Integer : public Object{
  int i_;
  Integer(int i) : i_{i}{}
}

struct Float : public Object{
  float f_;
  Float(float f) : f_{f}{}
}

By (ab-)using polymorphism I can now store those two types in a vector:

std::vector<Object*> object_list{new Integer(1), new Float(2.1), new Integer(3), new Float(4.2)};

But now I would like to add all those values together.

I can think of...

1) ...defining functions

Integer* add(Integer* i, Integer* j);
Float*   add(Integer* i, Float* f);
Float*   add(Float* f, Float* g);
Float*   add(Float* f, Integer* i);

But this would require to dynamically cast Object to all available types - twice, which seems like a catastrophe if I have enough children classes.

2) ... Templates, but that won't work, because the types are not known at compile time.

So what is the most efficient way regarding the following requirements:

*Execution time is more important than memory usage (although it should run on an 8GB system)

*It should support an arbitrary number of child classes, but must at least up to 20

*Is not limited to adding, but an arbitrary function f(Object* a, Object* b) should be supported

*The design of the classes is not yet fixed. If something works that requires change (or changing the total structure in it self) that is possible

*All possible types are known upfront, external DLLs do not need to be supported

*Does not need to support multiple inheritance

*Does not need to be robust in error handling. Recoverable would be nice but I can live with a SEGFAULT.

infinitezero
  • 1,610
  • 3
  • 14
  • 29
  • 1
    The code you describe and the code you post don't match up. `Integer` is not derived from `Object`. And even after adding that, dynamic cast won't work unless you do something else entirely. – Yakk - Adam Nevraumont Feb 04 '20 at 15:22
  • Thanks, fixed. What do you mean by restrictions? – infinitezero Feb 04 '20 at 15:24
  • Seems like you could use e.g. [`std::any`](https://en.cppreference.com/w/cpp/utility/any) or [`std::variant`](https://en.cppreference.com/w/cpp/utility/variant) instead of creating your own (non-polymorphic) inheritance hierarchy. – Some programmer dude Feb 04 '20 at 15:25
  • Second, what restrictions do you **require** (not want, not feel would be neat) about where the set of types deriving from `Object` need to be? Can there be a central list anywhere? When you define `add`, can it know all of the types? Do you **require** that someone else be able to add a DLL that adds a new type to the heirarchy and extends all of your functions? What are acceptable things to happen if there is a type that doesn't match any of those you support in `add`? Do you need to support multiple levels of inheritance? Be aware that saying "sure, that would be nice" is self sabotauge. – Yakk - Adam Nevraumont Feb 04 '20 at 15:25
  • 1
    This is sometimes called the "double dispatch" problem. https://en.wikipedia.org/wiki/Double_dispatch – NicholasM Feb 04 '20 at 15:26
  • I tried my best to address the requirements. Please let me know if there's still something unclear or missing – infinitezero Feb 04 '20 at 15:32
  • 1
    Since your types aren't polymorphic the dynamic cast won't work. No you did not fix the problem. Normally, people make virtual interface classes so they can restrict to usage of virtual functions only. Your usecase doesn't fit with the purpose of abstract classed. You'd better simply make several arrays - one for Interger, one for Float, and more for other types if needed. – ALX23z Feb 04 '20 at 15:40

2 Answers2

3
using Object = std::variant<Float, Integer>;

now you can have a std::vector<Object> and store Floats and Integers in it.

struct Integer {
  int val = 0;
  friend std::ostream& operator<<( std::ostream& os, Integer const& obj ) {
    return os << obj.val;
  }        
};
struct Float {
  double val = 0.;
  friend std::ostream& operator<<( std::ostream& os, Float const& obj ) {
    return os << obj.val;
  }        
};

using Object = std::variant<Integer, Float>;
std::ostream& operator<<( std::ostream& os, Object const& obj ) {
  // note: if the type in Object doesn't have a << overload,
  // this will recurse and segfault.
  std::visit( [&]( auto const& e ){ os << e; }, obj );
  return os;
}

Integer add_impl(Integer const& i, Integer const& j) { return {i.val + j.val}; }
Float   add_impl(Integer const& i, Float const& j) { return {i.val + j.val}; }
Float   add_impl(Float const& i, Float const& j) { return {i.val + j.val}; }
Float   add_impl(Float const& i, Integer const& j) { return {i.val + j.val}; }

Object  add( Object const& lhs, Object const& rhs ) {
  return std::visit( []( auto& lhs, auto& rhs )->Object { return {add_impl( lhs, rhs )}; }, lhs, rhs );
}

Test code:

Object a = Integer{7};
Object b = Float{3.14};
Object c = Integer{-100};
Object d = Float{0.0};

std::cout << add( a, b ) << "," << add( b, c ) << "," << add( c, d ) << "," << add( add(a, b), add( c, d ) ) << "\n";

this implements a dispatch table (more recent compilers will generate a far more efficient one) that will look for add overloads.

The return type is an Object but it will contain either a Float or an Integer at runtime.

The list of types you support needs to be at one spot, at the definition of Object. These objects don't have to be related types.

You can extend the add_impl in the namespace of the types in Object instead of in a central location. ADL will be used to find the overload set.

Of course, I'd implement operator+ instead of add.

There are some tricks you can use to fix:

// note: if the type in Object doesn't have a << overload,
// this will recurse and segfault.

that problem; basically something like:

namespace ObjectOnly {
  struct Object;
  struct Object:std::variant<Integer, Float> {
    using std::variant<Integer, Float>::variant;
    std::variant<Integer, Float> const& base() const& { return *this; }
    std::variant<Integer, Float> & base()& { return *this; }
    std::variant<Integer, Float> const&& base() const&& { return std::move(*this); }
    std::variant<Integer, Float> && base()&& { return std::move(*this); }
  };
  Object add_impl( Object const& lhs, Object const& rhs ) {
    return std::visit( [](auto& lhs, auto& rhs)->Object { return {lhs+rhs}; }, lhs.base(), rhs.base() );
  }
  Object operator+( Object const& lhs, Object const& rhs ) {
    return add_impl( lhs, rhs );
  }
  std::ostream& stream_impl( std::ostream& os, Object const& obj ) {
    std::visit( [&]( auto const& e ){ os << e; }, obj.base() );
    return os;
  }
  std::ostream& operator<<( std::ostream& os, Object const& obj ) {
    return stream_impl( os, obj );
  }
}

this will block add_impl from being able to see ObjectOnly::operator+. It will still be able to see operator+ in the same namespace as Float or Integer.

See here. If you edit Integer to not support << you'll get a compile-time instead of run-time error.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • This looks promising, I hadn't heard of `variant` and `visit` yet. I'll give it a try and accept then. There is a reason why I chose add over operator overloading, but that is of no concern here. – infinitezero Feb 04 '20 at 15:56
  • @infinitezero Sure, and there is a reason I didn't. "Behave like an `int` unless you have a great reason" is a good maxim to follow. :) – Yakk - Adam Nevraumont Feb 04 '20 at 16:00
  • This approach demands that I implement any combination of parameters available, which quickly becomes impractical. Is there a workaround for this? – infinitezero Feb 06 '20 at 00:33
  • @infinit I do not know. What do you want to do when the parameters don't have an implementation? Then do that? Are you asking how to write an overload that is called when none of the others match? Something else? I do not know. Yes, some code somewhere must soecify what hapoens for every pair of arguments. It could be a SFINAE test, a template, a converting constructor, or a pile of generated code. This simply calls `add_impl` with the actual types stored; what happens after that is up to your choice and the rules of function overloading (which is turing complete in C++) – Yakk - Adam Nevraumont Feb 06 '20 at 03:09
  • Yes, I'm looking for this `Are you asking how to write an overload that is called when none of the others match?` – infinitezero Feb 06 '20 at 10:50
  • I found an answer: https://stackoverflow.com/questions/60086331/throw-exception-on-missing-function-overload-with-stdvariant-instead-of-compil/60094231#60094231 – infinitezero Feb 06 '20 at 11:45
1

If you can choose a single type as the canonical "common" type, and provide a conversion from polymorphic types to that common type, then you can use that as the final and intermediary result of the sum.

For your example classes, a float object could be used to represent their value:

struct Object{
    operator float() = 0;
};

Then, you can calculate the sum with a loop:

float sum = 0;
for (Object* o : object_list) {
    sum += *o;
}
eerorika
  • 232,697
  • 12
  • 197
  • 326