C++ Abstract class can't have a method with a parameter of that class

后端 未结 4 1094
一向
一向 2021-02-04 04:25

I created this .h file

#pragma once

namespace Core
{
    class IComparableObject
    {
    public:
            virtual int CompareTo(IComparableObject obj)=0;
          


        
4条回答
  •  醉梦人生
    2021-02-04 04:42

    Well I have to give an unexpected answer here! Dennycrane said you can do this:

    virtual int CompareTo(IComparableObject const &obj)=0;
    

    but this is not correct either. Oh yes, it compiles, but it is useless because it can never be implemented correctly.

    This issue is fundamental to the collapse of (statically typed) Object Orientation, so it is vital that programmers using OO recognize the issue. The problem has a name, it is called the covariance problem and it destroys OO utterly as a general programming paradigm; that is, a way of representing and independently implementing general abstractions.

    This explanation will be a bit long and sloppy so bear with me and try to read between the lines.

    First, an abstract class with a pure virtual method taking no arguments can be easily implemented in any derived class, since the method has access to the non-static data variables of the derived class via the this pointer. The this pointer has the type of a pointer to the derived class, and so we can say it varies along with the class, in fact it is covariant with the derived class type.

    Let me call this kind of polymorphism first order, it clearly supports dispatching predicates on the object type. Indeed, the return type of such a method may also vary down with the object and class type, that is, the return type is covariant.

    Now, I will generalise the idea of a method with no arguments to allow arbitrary scalar arguments (such as ints) claiming this changes nothing: this is merely a family of methods indexed by the scalar type. The important property here is that the scalar type is closed. In a derived class exactly the same scalar type must be used. in other words, the type is invariant.

    General introduction of invariant parameters to a virtual function still permits polymorphism, but the result is still first order.

    Unfortunately, such functions have limited utility, although they are very useful when the abstraction is only first order: a good example is device drivers.

    But what if you want to model something which is actually interesting, that is, it is at least a relation?

    The answer to this is: you cannot do it. This is a mathematical fact and has nothing to do with the programming language involved. Lets suppose you have an abstraction for say, numbers, and you want to add one number to another number, or compare them (as in the OP's example). Ignoring symmetry, if you have N implementations, you will have to write N^2 functions to perform the operations. If you add a new implementation of the abstraction, you have to write N+1 new functions.

    Now, I have the first proof that OO is screwed: you cannot fit N^2 methods into a virtual dispatch schema because such a schema is linear. N classes gives you N methods you can implement and for N>1, N^2 > N, so OO is screwed, QED.

    In a C++ context you can see the problem: consider :

    struct MyComparable : IComparableObject {
      int CompareTo(IComparableObject &other) { .. }
    };
    

    Arggg! We're screwed! We can't fill in the .. part here because we only have a reference to an abstraction, which has no data in it to compare to. Of course this must be the case, because there are an open/indeterminate/infinite number of possible implementations. There's no possible way to write a single comparison routine as an axiom.

    Of course, if you have various property routines, or a common universal representation you can do it, but this does not count, because then the mapping to the universal representation is parameterless and thus the abstraction is only first order. For example if you have various integer representations and you add them by converting both to GNU gmp's data type mpz, then you are using two covariant projection functions and a single global non-polymorphic comparison or addition function.

    This is not a counter example, it is a non-solution of the problem, which is to represent a relation or method which is covariant in at least two variables (at least self and other).

    You may think you could solve this with:

    struct MyComparable : IComparableObject {
      int CompareTo(MyComparable &other) { .. }
    };
    

    After all you can implement this interface because you know the representation of other now, since it is MyComparable.

    Do not laugh at this solution, because it is exactly what Bertrand Meyer did in Eiffel, and it is what many people do in C++ with a small change to try to work around the fact it isn't type safe and doesn't actually override the base-class function:

    struct MyComparable : IComparableObject {
      int CompareTo(IComparableObject &other) {
         try 
           MyComparable &sibling = dynamic_cast(other);
           ...
        catch (..) { return 0; }
      }
    };
    

    This isn't a solution. It says that two things aren't equal just because they have different representations. That does not meet the requirement, which is to compare two things in the abstract. Two numbers, for example, cannot fail to be equal just because the representation used is different: zero equals zero, even if one is an mpz and the other an int. Remember: the idea is to properly represent an abstraction, and that means the behaviour must depend only on the abstract value, not the details of a particular implementation.

    Some people have tried double dispatch. Clearly, that cannot work either. There is no possible escape from the basic issue here: you cannot stuff a square into a line. virtual function dispatch is linear, second order problems are quadratic, so OO cannot represent second order problems.

    Now I want to be very clear here that C++ and other statically typed OO languages are broken, not because they can't solve this problem, because it cannot be solved, and it isn't a problem: its a simple fact. The reason these languages and the OO paradigm in general are broken is because they promise to deliver general abstractions and then fail to do so. In the case of C++ this is the promise:

    struct IComparableObject { virtual int CompareTo(IComparableObject obj)=0; };
    

    and here is where the implicit contract is broken:

    struct MyComparable : IComparableObject {
      int CompareTo(IComparableObject &other) { throw 00; }
    };
    

    because the implementation I gave there is effectively the only possible one.

    Well before leaving, you may ask: What is the right way (TM). The answer is: use functional programming. In C++ that means templates.

    template int compare(T,U);
    

    So if you have N types to compare, and you actually compare all combinations, then yes indeed you have to provide N^2 specialisations. Which shows templates deliver on the promise, at least in this respect. It's a fact: you can't dispatch at run time over an open set of types if the function is variant in more than one parameter.

    BTW: in case you aren't convinced by theory .. just go look at the ISO C++ Standard library and see how much virtual function polymorphism is used there, compared to functional programming with templates..

    Finally please note carefully that I am not saying classes and such like are useless, I use virtual function polymorphism myself: I'm saying that this is limited to particular problems and not a general way to represent abstractions, and therefore not worthy of being called a paradigm.

提交回复
热议问题