From: David Barrett-Lennard on 18 Apr 2010 21:04 Consider that we implement simple datatypes (i.e. simple "value types") without virtual functions and all read only functions are global functions (instead of class member functions). Let single argument constructors be used to support implicit type conversions. E.g. to emulate Square value is-a Rect value. The following uses structs (public member variables) without suggesting that is necessarily appropriate. struct Square { int s; }; int side(Square sq) { return sq.s; } struct Rect { Rect(Square s) : w(side(s)), h(side(s)) {} int w, h; }; int width(Rect r) { return r.w; } int height(Rect r) { return r.h; } int area(Rect r) { return width(r) * height(r); } void Test() { Square s = {10}; int A = area(s); } The width, height and area functions defined on rectangle values are also available for square values, which is just what we need, and could be viewed as a form of inheritance. By declaring these functions inline a modern compiler will typically eliminate temporaries and be as efficient as a specialisation for Square. Square inherits functions defined on rectangles but not the implementation of rectangle (i.e. the member variables). In fact Square can be implemented in any way we like. E.g. by recording the perimeter. struct Square { int perim; }; int side(Square sq) { return sq.perim/4; } Note that the implicit conversions are still available if the read only functions use const references. E.g. area could be declared like this int area(const Rect& r) { return width(r) * height(r); } Questions: 1) Is this a reasonable technique? 2) Is there a reason why C++ was designed so that const member functions defeat the implicit conversions on *this? -- [ See http://www.gotw.ca/resources/clcm.htm for info about ] [ comp.lang.c++.moderated. First time posters: Do this! ]
From: David Barrett-Lennard on 20 Apr 2010 04:44 On Apr 20, 10:35 pm, McNepp <mcnep...(a)googlemail.com> wrote: > I do understand that inheritance is not always the appropriate means > of expressing an 'is-a' relationship. > You show a fine example: it doesn't make sense for a Square to inherit > two values from a Rectangle if it only needs one value for a complete > description of its nature. > > However, I'd still look a the indisputable 'is-a' relationship and try > to model it in a way that makes the more specialized entity depend on > the less specialized. > > In your case, I'd suggest: remove the constructor that takes a Square > from Rectangle and add an 'operator Rectangle() const' to Square. Why exactly? Although it seems strange that a class is in effect declaring itself to be a supertype of another type, I can see at least one advantage. For example class Rational { public: Rational(int x) : num(x), denom(1) {} ... private: int num; int denom; }; This conceptually declares a supertype of a built-in type 'int'. I think C++ programmers normally associate a subtype relationship with subclassing and hence L-value-substitutability and not with (R-)value substitutability which is a different thing all together. This is despite the fact that C++ programmers commonly talk about data types or value-types. Anyway the result seems to be they don't consider short to be a subtype of int, or int to be a subtype of Rational. I'm assuming an L-value is an expression that refers to a memory location which may either be 1) a variable that holds an abstract value; or 2) a class instance (i.e. object) which doesn't represent an abstract value. In that case all we can say is that the object represents some finite state machine. I suggest L-value-substitutability only has to do with substitutability of one type of state machine for another, since it makes little sense to talk about substitutability of one type of variable for another. E.g. a circle variable can't be treated as an ellipse variable by a client because the client could illegally write an ellipse value. An ellipse variable cannot be treated as a circle variable because a client would erroneously assume reads of the variable only give circle values. > {http://www.parashift.com/c++-faq-lite/proper-inheritance.html#faq-21.6-mod } I read the ever popular circle-ellipse discussion in that link. I suggest it misses the point. It claims it can make sense for Circle to subclass Ellipse as long as Ellipse doesn't have a setSize() method that allows the width and height to be changed independently. Really, Circle and Ellipse are simple value-types and I cannot imagine not supporting appropriate assignment operators on variables of these types. A Circle variable cannot inherit the assignment operator that is applicable to an Ellipse variable, so subclassing cannot model the relationship correctly. Subclassing is unsuitable for subtype relationships between value types in general. Indeed ISTM user-defined coercions are really about addressing the fact that subclassing cannot model value substitutability correctly. Then again it looks to me that user- defined coercions are somewhat broken in C++ as well, which is a shame. -- [ See http://www.gotw.ca/resources/clcm.htm for info about ] [ comp.lang.c++.moderated. First time posters: Do this! ]
From: McNepp on 19 Apr 2010 23:35 I do understand that inheritance is not always the appropriate means of expressing an 'is-a' relationship. You show a fine example: it doesn't make sense for a Square to inherit two values from a Rectangle if it only needs one value for a complete description of its nature. However, I'd still look a the indisputable 'is-a' relationship and try to model it in a way that makes the more specialized entity depend on the less specialized. In your case, I'd suggest: remove the constructor that takes a Square from Rectangle and add an 'operator Rectangle() const' to Square. { http://www.parashift.com/c++-faq-lite/proper-inheritance.html#faq-21.6 -mod } -- [ See http://www.gotw.ca/resources/clcm.htm for info about ] [ comp.lang.c++.moderated. First time posters: Do this! ]
From: David Barrett-Lennard on 21 Apr 2010 22:09 On Apr 22, 3:35 am, Keith H Duggar <dug...(a)alum.mit.edu> wrote: > On Apr 19, 8:04 am, David Barrett-Lennard <davi...(a)iinet.net.au> > wrote: > > > The width, height and area functions defined on rectangle values are > > also available for square values, which is just what we need, and > > could be viewed as a form of inheritance. By declaring these > > functions inline a modern compiler will typically eliminate > > temporaries and be as efficient as a specialisation for Square. > > It's possible is /could/ be viewed as a "form" of inheritance, > but that would of course depend on how you define inheritance. > It certainly is not inheritance in the C++ definition. Agreed > And no > matter what definition you choose, if it allowed this to be a > "form of inheritance" I would argue that said definition is at > total odds with any common sense meaning of "inheritance". I was thinking of it as "inheritance" in the following sense: Let a data type mean a set of abstract values plus operators on those values (using operator in an algebraic sense). The "behaviour" of a datatype is only externally visible through those operators. Relating this back to C++ code, assuming all operators are expressed as free functions without in-out parameters, an implicit conversion between two datatypes means that all the operators of one are available to the other - because of value substitutability. I think it is reasonable to call that "inheritance" treating the word as simply meaning that "stuff" associated with one "thing" is automatically available to another "thing". Anyway that was the only point I wanted to make! I just looked for synonyms of "inherit" at synonym.com and the only alternatives it gave me are "get" and "acquire". > Suppose I create a coercion from complex<int,int> to rectangle, > does complex<int,int> now "inherit" from rectangle? What if we > add a coercion from rectangle to complex<int,int>, does rectangle > no circularly inherit from complex? That's just weird so I'm not sure what your point is here. A better example for me would be classes named Polar and Cartesian and we would like implicit conversions in both directions. I haven't studied the rules of implicit conversions for C++ to know whether such a thing works in practise, but in principle I can't see any problems with alternative representations of abstract values. BTW the conversions between polar and cartesian representations involve cos, sin, sqrt, atan2 which are inexact for rational numbers (including floats). That could mean we have an example of the following assertion breaking which I find extremely unappealing: T1 x; T2 y = x; assert(x == y); This can fail if coercions aren't invertible and y is coerced in order to perform comparison using T1::operator==. On that basis I would reject implicit conversions between polar and cartesian representations. I would hope that implicit conversions between datatypes satisfy reasonable "laws" to make program correctness easy to reason about. Unfortunately C++ doesn't give you a transitive closure of the defined implicit conversions. Consider that we extend my previous example with an additional type to represent an arbitrary quadrilateral and we recognise that square value is-a rectangle value is-a quadrilateral value. In addition, assume that we haven't defined an area function specialised for squares or rectangles. Unfortunately the following won't compile: struct Quad { Quad(Rect r) { x[0] = width(r); y[0] = 0; x[1] = width(r); y[1] = height(r); x[2] = 0; y[2] = height(r); } int x[3]; int y[3]; }; int area(Quad q) { return ( - q.x[0]*q.y[0] + (q.x[0]-q.x[1])*(q.y[0]+q.y[1]) + (q.x[1]-q.x[2])*(q.y[1]+q.y[2]) + q.x[2]*q.y[2] ) / 2; } void Test() { Square s = {10}; int A = area(s); // error: no implicit conversion } In any case even a modern C++ compiler wouldn't optimise the code adequately (and that's why "overrides" would be very useful even though logically they are redundant). I would expect that whenever there are cycles in the value substitutability graph then in principle we would expect implicit conversions to be available from any one to any other type in the cycle. But that's going beyond C++ capabilities. > What about all the numerous > other types we made add in future, are we now talking about > multiple inheritance? In the sense of inheritance I described above, definitely! Related to this are union types which unfortunately aren't supported adequately in C++. > Not anywhere close in my mind. How would > such "inheritance" thinking or terminology even be useful? In the case of square value isa rectangle value, it is useful in the sense that it is a reminder that one doesn't necessarily need to implement operators on square that have already been written for rectangles. If one forgets that then one may end up writing more code than needed (and the C++ compiler won't complain of course). > > Square inherits functions defined on rectangles but not > > No it doesn't "inherit" the functions. Instead you have created > an algebra in which an implicit coercion makes syntax such as > > width(square) > > valid. Suppose I now declare and explicit function for square > > int area ( Square s ) { return side(s) * side(s) ; } > > would you now propose to say that I have "overridden the > inherited (from rectangle) area function"? That doesn't make > any bit of sense to me. "inheritance" has no place here. I'm not sure why. I thought the "override" metaphor was quite a good one. I would however say it's a very dangerous analogy because it means something quite different to overrides of virtual methods in the context of subclassing. However, on the other hand we are talking about data types here and we know that subclassing is completely and universally useless for data types so I would suggest that confusion cannot occur for C++ programmers that properly understand datatypes. I would say that such an "override" must not be allowed to change the externally visible behaviour, but it can be useful to optimise performance when the compiler is inadequate. For any data types T1,T2 and unary operator foo I don't believe the assertion below should be allowed to fail: T1 x; T2 y = x; assert(foo(x) == foo(y)); "overrides" would need to respect that (for example). -- [ See http://www.gotw.ca/resources/clcm.htm for info about ] [ comp.lang.c++.moderated. First time posters: Do this! ]
From: David Barrett-Lennard on 22 Apr 2010 08:18 On Apr 22, 6:59 pm, �� Tiib <oot...(a)hot.ee> wrote: > On Apr 19, 3:04 pm, David Barrett-Lennard <davi...(a)iinet.net.au> > wrote: > > > The following uses structs (public member variables) without > > suggesting that is necessarily appropriate. > > > struct Square { int s; }; > > int side(Square sq) { return sq.s; } > > You pass Square by value, and that is bad idea when Square represents > something with enough properties (that even squares have like > location, color, inclination degree ... etc.) to pass it by reference. > Better is: > > int side(Square const& sq) { return sq.s; } Yes I implied that was a valid option in the original post. With in- lining it's unlikely to make any difference. In this particular case if the compiler really did pass by reference it could well be slower because of the indirection. > > struct Rect > > { > > Rect(Square s) : w(side(s)), h(side(s)) {} > > This causes often secret overhead, you may get conversion copy where > you did not expect it. I usually put explicit in front of it. Yes but the whole point of this exercise was to allow implicit conversions. > > void Test() > > { > > Square s = {10}; > > int A = area(s); > > } > > If you did not make Rect constructor explicit then it works. Otherwise > you have to write: > int A = area(Rect(s)); > > That indicates to possible reader that conversion takes place. So if > they want to optimize (and graphics people often do) then they write: > > inline int width(Square const& sq) { return side(sq); } > inline int height(Square const& sq) { return side(sq); } > inline int area(Square const& sq) { return > width(sq)*height(sq); } > > As results most compilers generate equivalent of: > int A = 100; That happens with modern compilers anyway with my original example, so writing all that extra code is unnecessary. > > Questions: > > > 1) Is this a reasonable technique? > > I avoid it. In large project such implicit conversions cause sometimes > hard to discover bugs because they hide simple typos. Implicit conversions can be classified as safe or dangerous depending on whether there are surprises. I believe I gave a safe one that is very unlikely to be the source of bugs. Indeed all things being equal, more concise code tends to have fewer bugs. If you think otherwise I would appreciate a realistic example. > > 2) Is there a reason why C++ was designed so that const member > > functions defeat the implicit conversions on *this? > > I don't think that they do. I think that your own const-correctness > and pass-by-value defects defeat it. Try if it works: Where was there a const-correctness defect? Pass by value is hardly a "defect" (at least in the code I presented). > > struct Square { int s; int foo() const; }; > int side(Square const& sq) { return sq.s; } > > struct Rect > { > Rect(Square const& s) : w(side(s)), h(side(s)) {} > int w, h; > }; > int width(Rect const& r) { return r.w; } > int height(Rect const& r) { return r.h; } > int area(Rect const& r) { return width(r) * height(r); } > int Square::foo() const { return area(*this); } > > void Test() > { > Square s = {10}; > int A = area(s); > int F = s.foo(); > } Actually I meant that a const member function Rect::area() cannot allow implicit conversions when attempting to use it to find the area of a Square. The result is that in C++ free functions are much better behaved for value-types. Indeed this is a common reason for using friend functions. -- [ See http://www.gotw.ca/resources/clcm.htm for info about ] [ comp.lang.c++.moderated. First time posters: Do this! ]
|
Next
|
Last
Pages: 1 2 3 4 Prev: What's the lifetime of temporaries in lambdas? Next: implicit conversion: ctor or cast? |