Polymorphism and encapsulation

This topic is 3702 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

Recommended Posts

Is the requirement that virtual functions must be members of the class they are polymorphic over in conflict with the admonition that, where possible, behavior should be implemented in free functions rather than member functions?

Share on other sites
I don't see this as a conflict. If the base class is saying a method is virtual, then it follows that it should be part of the derived class. If the virtual nature of it could be done as a means of extension, I think you could argue that it shouldn't be part of the base class either. Therefore, you might have a separate hierachy which might have the extension methods, but then this could be outside the class.

So you could have something like:
Base -> Derived
BaseHelper -> DerivedHelper (Though I'm not so fond of "helpers" since they don't really represent an object in the system).

I guess you could also argue that, at least in C++, you're not necessarily limited to this methodology, since you also have function pointers, which would give you the capability you're talking about.

Those are my thoughts, anyway.

Share on other sites
Make public non-virtual functions (or, preferably, non-member functions), and private virtual functions. They work together, so it should not be in conflict, these are two separate things : implementation and interface.

Share on other sites
Mmm. Said admonition arises from a couple points:

1) It's best to write generic code that's not hardcoded to anything specific. Members are by definition hardcoded to their class. Virtual functions, however, are also hardcoded by definition to their class (and would be as nonmembers as well), meaning no additional restrictions are imposed by this (WRT this point).

2) It's best for maintainability to write as much of your code against the public interface as possible rather than the private internals, on account of it reducing the refactoring cost of internal changes. While requiring virtuals to be members conflicts with the ability to encourage this via the mechanics of the language (by not giving said method access to the class implementation details), it doesn't conflict with the ability to only write against said public interface.

I suppose the answer is "I suppose so", then.

Share on other sites
Quote:
 Original post by rolkAMake public non-virtual functions (or, preferably, non-member functions), and private virtual functions. They work together, so it should not be in conflict, these are two separate things : implementation and interface.

That's not really what I'm getting at. MaulingMonkey said it well. There's no overriding reason for the function to be a member function at all, except for its need to be blessed by the class to have dynamic polymorphism.

Share on other sites
Writing an efficient virtual member pretty much requires structural changes in the class code for a given class.

Virtual functions thus intertwine tightly with the class definition.

Writing non-member virtual functions is possible in C++, but it is inefficient and awkward. In C++ at least, the "has access to the inner workings" and "can change the layout of the class and instances" are intertwined concepts -- so an efficient elegant non-member virtual function isn't that reasonable.

One could imagine a language in which arguments to a function are virtual. How one would implement this both efficiently (at least, as efficiently as a virtual member of a class in C++ does) and without having to rework the layout of the class (not the layout of instances -- the class has a data-representation in C++ runtime, and many other language as well) is difficult for me to imagine.

So, in C++, you cannot implement the functionality of a truly virtual function outside of a member function. You could refactor the components that are virtual in that object and put them into member functions, then have a non-member function that does the "main" work and uses the virtual member function for the isolated virtual behavior.

There are also reflection tricks you can pull off, where you pass a functor into a class, which is then called back from the class with the "virtual" override type of the argument. Ie:

class Base;class A;class B;class DoStuff {  public:    virtual void Work(Base&) = 0;    virtual void Work(A& a);    virtual void Work(B& b);};void DoStuff::Work(A& a) {Work(static_cast<Base&>(a));}void DoStuff::Work(B& b) {Work(static_cast<Base&>(b));}class Base {  public:    virtual void Reflect( DoStuff& ) = 0;};void Base::Reflect(DoStuff& stuff) { stuff.Work(*this); }class A: public Base {  public:    virtual void Reflect( DoStuff& stuff ) { stuff.Work(*this); }};class B: public Base {  public:    virtual void Reflect( DoStuff& stuff ) { stuff.Work(*this); }};class Hidden: public Base {  public:    virtual void Reflect( DoStuff& stuff ) { Base::Reflect(stuff); }};

Here we have a kind of manual reflection. Any class that implements the DoStuff methods can now call a Base object and have it reflect the actual internal type (from a finite list).

Note that the strange trick (pure virtual methods with implementations) is done on purpose. It means that children can call the parent for a default implementation, but they must override the method and do so manually. You can see me doing this in class Hidden.

There is also a fair amount of manual drudgery that has to be done to keep such a Reflection setup working. Whenever a new class is added, it needs to be added to the DoStuff() interface. And you must maintain the tree manually within DoStuff(), so base versions of classes can automatically be handled by parent versions.

However, in the end you get pretty fast non-member virtual arguments. With a bit of work, you could even extract the actual types of multiple members, and then call something like a template functor with free arguments that can deal with the objects "as they really are" if needed...

Share on other sites
Quote:
 Original post by SneftelIs the requirement that virtual functions must be members of the class they are polymorphic over in conflict with the admonition that, where possible, behavior should be implemented in free functions rather than member functions?

The restriction that virtual functions cannot be polymorphic based on their parameters (except for calling instance) is at odds with the general directive that behavior be free functions.

It's interesting because I've been dealing with this a little bit in my current hobby project which is a toy programming language. It supports polymorphism on free functions, though it has a catch. What happens with this?:

void foo(object, object);void foo(int, object);void foo(object, int);foo(1,2);

What does the language do? What option is 'best'?

Personally, I have this checked at compile time which will require you to define a most explicit void foo(int,int). Runtime yields exceptions. But that checking is computationally more expensive than traditional virtual methods. And it doesn't eliminate the added dispatch time on the virtual invoke. Are either of those prohibitively more expensive for the feature? Maybe. I've not got enough working to make that evaluation yet...

But I suspect that the requirement that virtual methods be members of the polymorhpic element isn't a requirement as much as a restriction that tries to best balance utility and the difficulty/ambiguity that arises when it's not there.

And really, it's not a conflict as much as a trade-off.

Share on other sites
In C++, you have a class hierarchy that means that the compiler knows, to a certain degree, how classes are related.

At compile-time, types are checked against the class hierarchy. If you call a function that doesn't match the static type check, it complains.

If the argument is virtual (ie, does dynamic dispatch), the object types that it can dispatch to are the child objects of the interface type.

For simplicities sake, I'd require that the dispatch order either be specified by the programmer: probably with a default key-word based option to make it left-to-right dispatch lookup.

Then your code might look like:
interface void foo(virtual base& arg1, virtual base& arg2); // 1// later:virtual foo default order;override void foo(virtual child& arg1, virtual base& arg2); // 2override void foo(virtual base& arg1, virtual child& arg2); // 3override void foo(virtual base& arg1, virtual child2& arg2); // 4foo(base(), base()); // 1foo(child(), base()); // 2foo(base(), child()); // 3foo(child(), child()); // 2, because we do the virtual lookup on arg1 firstfoo(base(), child2()); // 4foo(child(), child2()); // 2, because the first argument has virtual lookup first

...

Of course, there is the trick you can pull off in C++ where your argument type has little to do with the parameters that the function actually gets.

Ie, in C++:
struct Functor {  struct ArgumentParser {    bool is_int;    bool is_class;    int v;    class_type c;    ArgumentParser():is_int(false), is_class(false) {};    ArgumentParser(class_type const& c_):c(c_), is_int(false), is_class(true) {};    ArgumentParser(int const& v_):v(v_), is_int(true), is_class(false) {};    bool get_int(int& v_){ if (!is_int) return false; v_=v; return true;}    bool get_class(class_type& v_){ if (!is_class) return false; v_=c; return true;}  };  void operator()(ArgumentParser arg1, ArgumentParser arg2) const {    // code in here uses pre-massaged data  }};

where the type of an argument in the argument list can express an entire set of operations that are done based off of the types and values of the arguments passed in.

(Note that the above artificial argument parser example is, well, artificial. As an example of where it is good, imagine a comparison operator that implements an ordering on two different class types. By putting the logic into the arguments, you need only one override of the function instead of 4 overrides.)

struct ordering {  struct argument {    int value;    argument(type1 const& t):value(t.value()){};    argument(int const& i):value(i){};    bool operator()(argument a, argument b)const{return a.value<b.value;}  };};// vsstruct ordering2 {  bool operator()(int a, int b)const{return a<b;}  bool operator()(int a, type1 const& b)const{return a<b.value();}  bool operator()(type1 const& a, int b)const{return a.value()<b;}  bool operator()(type1 const& a, type1 const& b)const{return a.value()<b.value()};};

already at 2 arguments, we require 2^2 or 4 overloads, each of which can contain a typo or unexecuted code that rots on you.

If you had a hypothetical trinary function, it would require 8 overloads without doing the "argument that extracts what we need from the incoming information" technique. And it continues blowing up exponentially.

Share on other sites
Quote:
 Original post by SneftelIs the requirement that virtual functions must be members of the class they are polymorphic over in conflict with the admonition that, where possible, behavior should be implemented in free functions rather than member functions?

If the behaviour needs to be polymorphic, how are you expecting to do it via free functions? If you universally do it the obvious way (via dynamic_cast), then you've missed the point entirely. So, no. Polymorphic behaviour is a reason for free-function-implementation to be "impossible".

My interpretation of said admonition is that all we're trying to do is keep the object's definition from spiralling out of control.

Quote:
 Original post by TelastynWhat does the language do? What option is 'best'? Personally, I have this checked at compile time which will require you to define a most explicit void foo(int,int).

This is what I planned for my language, too. (However, the (object, object) version would *not* be required, unless foo() is called in a context where it isn't known that either parameter is an int.)

Share on other sites
Quote:
 Original post by Sneftelbehavior should be implemented in free functions rather than member functions?

Where does this idea come from? Never heard of it.

1. 1
2. 2
Rutin
23
3. 3
JoeJ
20
4. 4
5. 5

• 28
• 40
• 23
• 13
• 13
• Forum Statistics

• Total Topics
631737
• Total Posts
3001945
×