10.5 Class and file organization for PPoint and PLineBecause of the duality between points and lines in the projective plane, most ofthe data and code we use to represent these concepts
Trang 135 B.print(); cout << endl;
36 // cout << B.sum() << endl; // Illegal, sum is protected
TheBaseclass has two data members: a private integeraand a protected integer
b The class also includes a protected method namedsum, a public constructor, and
a public method namedprint
ClassChildhas no additional data members It has a public methodincrease_bthat increases the data memberb by one Note that it would not be possible forChildto have a similar method for increasinga The constructor forChildpassesits arguments on to its parent,Base, but then takes no further action (hence the curlybraces on line 17 do not enclose any statements)
Theprintmethod forChildusesBase’sprintmethod andsummethod.Class GrandChildadds an extra private data element, c The constructor forGrandChildpasses its first two arguments to its parent’s constructor and then usesthe third argument to set the value ofc
Theprintmethod forGrandChilduses its grandparent’sprintmethod thoughBase::print()invokes thesummethod, theGrandChildmethods cannotdirectly callsumbecause it is protected inBase, hence implicitly private inChild,and hence inaccessible inGrandChild
Al-Amainto illustrate all these ideas begins on line 30 The output of the programfollows
Trang 210.5 Class and file organization for PPoint and PLine
Because of the duality between points and lines in the projective plane, most ofthe data and code we use to represent these concepts in C++ are the same If at allpossible, we should avoid writing the same code twice for two reasons First, theinitial work in creating the programs is doubled More important, maintaining thecode is also made more difficult; if a change is required to the code, we need toremember to make that change twice When we are fussing with our programs andmaking a number of minor modifications, it is easy to forget to update both versions
To illustrate this, we deliberately include a subtle flaw in the first version of theprogram; we then repair the problem once (not twice)
To this end, we define three classes: a parent class namedPObjectthat contains
as much of the code as possible and two derived classes,PPointandPLine, thatinclude code particular to each These classes are defined in two files each: a header.hfile and a code.ccfile Figure 10.3 illustrates the organization
PObject
PObject.h PObject.cc
PPoint
PPoint.h PPoint.cc
PLine
PLine.h PLine.cc
Figure 10.3: Hierarchy of the PObject classes
The header filePObject.his used to declare thePObjectclass BothPPoint.handPLine.h require the directive#include "PObject.h" Programs that useprojective geometry need all three Rather than expecting the user to type multiple
#includedirectives, we create a convenient header file that includes everything weneed We call this header fileProjective.h; here it is
Program 10.3: Header file for all projective geometry classes, Projective.h
Trang 3Notice that we do not require#include "PObject.h" because that file is ready#included byPPoint.handPLine.h All these files have the usual struc-ture to prevent multiple inclusion.
al-All told, we have seven files that implement points and lines in the projectiveplane
PObject.h The header file for thePObjectclass
PObject.cc The C++ code for thePObjectclass
PPoint.h The header file for thePPointclass
PPoint.cc The C++ code for thePPointclass
PLine.h The header file for thePLineclass
PLine.cc The C++ code for thePLineclass
Projective.h The only header file subsequent programs need to include to use
projective points and lines
The decision to write the program in seven different files is based on the principle
of breaking a problem down to manageable sizes and working on each piece arately We could have packed all this work into two larger files (one.hand one.cc)
sep-10.6 The parent class PObject
Data and functionality common to PPointandPLine are implemented in theclass PObject Using the ideas presented in Section 10.2, we map out the classPObject
Data A point or line inRP2is represented by homogeneous coordinates:(x,y,z) or [x,y,z] To hold these coordinates, we declare three private double variables,
x,y, andz Let us writex,y,z for the homogeneous coordinates of a generic
projective object (either a point of a line) (See Program 10.4, line 9.)Because 2,1,−5 and −4,−2,10 name the same object, it is useful to
choose a canonical triple One idea is to make sure thatx,y,z is a unit vector
(but then we have a sign ambiguity) The representation we use is to make thelast nonzero coordinate of the triple equal to 1 The motivation for this choice
is that a point in the Euclidean plane at coordinates(x,y) corresponds to the
point(x,y,1) in the projective plane.
If later we are unhappy with this decision, we can choose another manner
to store the homogeneous coordinates Because the coordinates are privatemembers ofPObject, we would only need to update the code forPObject
Trang 4We provide public methodsgetX(), getY(), andgetZ() to inspect (butnot modify) the values held inx,y, andz, respectively (See Program 10.4lines 33–35.)
We reserve0,0,0 to stand for an invalid projective object We provide a
pub-lic methodis_invalid()to check if an object is invalid (See Program 10.4lines 37–39.)
Constructors It is natural to define a constructor for aPObjectwith three ters that set the homogeneous coordinates of the object The user might invokethe constructor like this:
parame-PObject P(2., 3., 5.);
Rather than holding this point as2,3,5, we use the canonical representation
0.4,0.6,1 (See Program 10.4 lines 25–30.)
To facilitate the conversion of user-supplied coordinates to canonical nates, we create a private helper procedurescale (See Program 10.4 line 10and Program 10.5 lines 4–19.)
coordi-All C++ classes ought to define a zero-argument constructor that creates adefault object of that class In this case, a sensible choice is the object0,0,1.
This corresponds to the origin (as a point) or the line at infinity (as a line) (SeeProgram 10.4 lines 21–24.)
Relations We want to be able to compare projective points (and lines) for equality,
inequality, and< (for sorting) To this end, we might be tempted to define
operator==in the public portion ofPObjectlike this:
public:
bool operator==(const PObject& that) const {
return ( (x==that.x) && (y==that.y) && (z==that.z) );}
Although this would give the desired result when comparing points to points
or lines to lines, it would also provide the unfortunate ability to compare points
to lines Were we to use the above method for testing equality, then we mightrun into the following situation
There are at least two problems with this approach First, the point(2,3,5) and
the line[2,3,5] are not equal even though they have the same homogeneous
Trang 5coordinates Second, lines and points should not even be comparable by==.Code that compares a point to a line is almost certainly a bug; the best thing inthis situation is for the compiler to flag such an expression as an error.
We therefore take a different approach to equality testing We want the damental code that checks for equality to reside inside thePObjectclass be-cause that code is common to bothPPointandPLine We do this by declar-ing a protected method called equalsinside PObject (see Program 10.4line 14):
fun-protected:
bool equals(const PObject& that) const;
In the filePObject.ccwe give the code for this method (see Program 10.5lines 34–36):
bool PObject::equals (const PObject& that) const {
return ( (x==that.x) && (y==that.y) && (z==that.z) );}
Then, in the class definitions forPPointandPLinewe give the necessaryoperator definitions For example, inPLinewe have this:
Meet/Join Operation Given twoPPointsPandQ, we wantP+Qto return the linethrough those points Dually, givenPLinesLandM, we wantL*Mto returnthe point of intersection of these lines
In both cases, the calculations are the same: given the triplesx1,y1,z1 and
x2,y2,z2 we need to find a new triple x3,y3,z3 that is orthogonal to the first
two To do this, we calculate the cross product:
x3,y3,z3 = x1,y1,z1 × x2,y2,z2.
Therefore, inPObjectwe declare a protected method calledopthat is invoked
byoperator+ inPPointand operator*inPLine (See Program 10.4line 18 and Program 10.5 lines 138–148.)
Trang 6Incidence Given a point and a line, we want to be able to determine if the point lies
on the line If the coordinates for these are(x,y,z) and [a,b,c], respectively, then we simply need to test if ax + by + cz = 0.
It does not make sense to ask if one line is incident with another, so we do notmake an “is incident with” method publicly available inPObject Rather, wemake a protectedincidentmethod (that calls, in turn, a privatedotmethodfor calculating dot product) (See Program 10.4 lines 11,16 and Program 10.5lines 21–23,47–49.)
Then,PPointandPLinecan declare their own public methods that accessincident Details on this later
Collinearity/Concurrence Are three given points collinear? Are three given lines
concurrent? If the three objects have coordinatesx1,y1,z1, x2,y2,z2, and
x3,y3,z3, then the answer is yes if and only if the vectors are linearly
depen-dent We check this by calculating
Random points/lines There is no way to generate a point uniformly at random in
the Euclidean plane, but there is a sensible way in which we can do this forthe projective plane Recall that points inRP2correspond to lines through theorigin inR3 Thus, to select a point at random inRP2 we generate a pointuniformly at random on the unit ball centered at the origin An efficient way to
perform this latter task is to select a vector v uniformly in[−1,1]3 If v > 1,
then we reject v and try again.
The method for generating a random line is precisely the same
We therefore include a public methodrandomizethat resets the coordinates
of thePObjectby the algorithm we just described (See Program 10.4 line 31and Program 10.5 lines 25–32.)
To choose a random line through a point is similar Suppose the point is
(x,y,z) The line [a,b,c] should be chosen so that [a,b,c] is orthogonal to (x,y,z) To do this, we find an orthonormal basis for (x,y,z) ⊥that we denote
{(a1,b1,c1),(a2,b2,c2)} We then choose t uniformly at random in [0,2π].The random line[a,b,c] is given by
a = a1cost + a2sint b = b1cost + b2sint c = c1cost + c2sint.
Trang 7Rather than generate t and then compute two trigonometric functions, we can
obtain the pair(cost,sint) by choosing a point uniformly at random in the unit
disk (inR2) and then scaling
The technique for selecting a random point on a given line is exactly the same.Thus, we define a protectedrand_perpmethod inPObject(Program 10.4line 17 and Program 10.5 lines 79–136)
Therand_perpmethod is used byrand_pointinPLineandrand_line
14 bool equals(const PObject& that) const;
15 bool less(const PObject& that) const;
16 bool incident(const PObject& that) const;
17 PObject rand_perp() const;
18 PObject op(const PObject& that) const;
Trang 834 double getY() const { return y; }
35 double getZ() const { return z; }
36
37 bool is_invalid() const {
38 return (x==0.) && (y==0.) && (z==0.);
21 double PObject::dot(const PObject& that) const {
22 return x*that.x + y*that.y + z*that.z;
34 bool PObject::equals(const PObject& that) const {
35 return ( (x==that.x) && (y==that.y) && (z==that.z) );
36 }
37
38 bool PObject::less(const PObject& that) const {
39 if (x < that.x) return true;
Trang 940 if (x > that.x) return false;
41 if (y < that.y) return true;
42 if (y > that.y) return false;
43 if (z < that.z) return true;
73 double det = a1*b2*c3 + a2*b3*c1 + a3*b1*c2
74 - a3*b2*c1 - a1*b3*c2 - a2*b1*c3;
75
76 return det == 0.;
77 }
78
79 PObject PObject::rand_perp() const {
80 if (is_invalid()) return PObject(0,0,0);
81
82 double x1,y1,z1; // One vector orthogonal to (x,y,z)
83 double x2,y2,z2; // Another orthogonal to (x,y,z) and (x1,y1,z1) 84
85 if (z == 0.) { // If z==0, take (0,0,1) for (x1,y1,y2)
Trang 1096 else { // y and z both nonzero, use (0,-z,y)
143 double c1 = y*that.z - z*that.y;
144 double c2 = z*that.x - x*that.z;
145 double c3 = x*that.y - y*that.x;
146
147 return PObject(c1,c2,c3);
148 }
Trang 1110.7 The classes PPoint and PLine
With the code forPObjectin place, we are ready to finish our work by writing thefiles for the classesPPointandPLine We do this work in four files: PPoint.h,PPoint.cc,PLine.h, andPLine.cc(Programs 10.6 through 10.9)
As we start to write these files, we meet a chicken-and-egg problem Which do
we define first: the class PPointor the class PLine? For us, this is more than
a philosophical conundrum SomePLinemethods need to refer toPPoints Forexample, operator*acts on lines to produce points and PLine’srand_pointmethod returns aPPoint Dually, some of thePPointmethods require thePLineclass
Here is how we solve this dilemma In the PPoint.hfile, before we give thedefinition ofPPointwe have the following statement (see line 6 of Program 10.6),
Dually, we include the statementclass PPoint;in the filePLine.h
We focus our attention on the classPPoint The analysis ofPLineis similar.For the classPPointwe have five constructors At first, this may seem to be toomany, but we show how to do this easily
Of course, we want a three-argument constructorPPoint(a,b,c)that createsthe point(a,b,c) We also want a zero-argument constructorPPoint()that createsthe point(0,0,1) corresponding to the origin.
It makes sense to define a two-argument constructorPPoint(a,b)to create thepoint(a,b,1) In a sense, this maps the Euclidean point (a,b) to its natural corre-
spondent in theRP2
What should be the action of a single-argument constructor? The real number a
can be identified with the point(a,0) on the x-axis, and this in turn corresponds to (a,0,1) in RP2 In summary, we have the following constructors and their effects
Constructor form Point createdPPoint P(); (0,0,1)
PPoint P(a); (a,0,1)
PPoint P(a,b); (a,b,1)
PPoint P(a,b,c); (a,b,c)
The great news is that we can implement these four constructors with a single
definition using default parameters.
Trang 12For any C++ procedure (class method or free-standing procedure), default valuescan be specified In the.hfile, where procedures are declared, we use a syntax such
as this:
return_type procedure_name(type arg1 = val1, type arg2 = val2, );
Then, when the procedure is used, any missing parameters are replaced by their fault values Let’s look at a concrete example We declare a procedure namednextthat produces the next integer after a given integer (This is a contrived example, but
de-we want to keep things simple.) In the header file de-we put the following,
int next(int num, int step=1);
And in the.ccfile, we have the code,
int next(int num, int step) {
return num+step;
}
Notice that the argumentstepis given a default value,1 However, the argumentnumis not given a default It is permissible to specify only a subset of the argumentsthat receive default values, but if an argument has a default value, all arguments toits right must also have default values
Notice that the optional arguments are not reiterated in the.ccfile
Consider the following code
int a,b,c;
a = next(5);
b = next(5,1);
c = next(5,2);
This will setaandbequal to 6 andcequal to 7
Alternatively, we could have given an inline definition ofnextin the header filelike this:
inline int next(int num, int step=1) { return num+step; }
Returning toPPoint, the four constructors (with zero to threedoublearguments)can all be declared at once like this:
PPoint(double a=0., double b=0., double c=1.)
See line 10 of Program 10.6 The action of this constructor is simply to pass thethree arguments up to its parent (line 11) and then there is nothing else to do Toshow there is nothing else, we give a pair of open/close braces that enclose emptyspace (line 12)
The class PPointneeds one more constructor Recall that PObjectprovidesmethods such asrand_perpandopthat returnPObjects However, when these areused byPPointorPLine, thePObjectvalues need to be converted to typePPoint
orPLine as appropriate To do thisPPointprovides a constructor that accepts asingle argument of typePObject Here is the simple code (see also lines 14–15 ofProgram 10.6)
Trang 13PPoint(const PObject& that) :
PObject(that.getX(), that.getY(), that.getZ()) { }
This code sends thex,y, andzvalues held inthatup to the parent constructor andthen does nothing else Now, if we want to assign aPObjectvalue to aPPointobject, we can do it in the following ways
R = X; // implicit conversion (compiler figures out what to do)
Here is thePPoint.hheader file
Program 10.6: Header file for the PPoint class
14 PPoint(const PObject& that) :
15 PObject(that.getX(), that.getY(), that.getZ()) { }
Trang 14The parts ofPPointnot given inPPoint.hare defined inPPoint.ccwhich wepresent next.
Program 10.7: Program file for the PPoint class
1 #include "Projective.h"
2
3 ostream& operator<<(ostream& os, const PPoint& P) {
4 os << "(" << P.getX() << "," << P.getY() << "," << P.getZ() << ")";
Trang 1514 PLine(const PObject& that) :
15 PObject(that.getX(), that.getY(), that.getZ()) { }
3 ostream& operator<<(ostream& os, const PLine& P) {
4 os << "[" << P.getX() << "," << P.getY() << "," << P.getZ() << "]";
Trang 1617 return incident(that);
18 }
10.8 Discovering and repairing a bug
With the projective point and line classes built, it is time to test our code Here is
a simple main to perform some checks
Program 10.10: A main to test theRP2classes
20 cout << "Is P on L? " << P.is_on(L) << endl;
21 cout << "Does M have P? " << M.has(P) << endl;
28 cout << "Is Q on L? " << Q.is_on(L) << endl;
29 cout << "Does M have Q? " << M.has(Q) << endl;
Trang 17When this program is run, we have the following output:
The random point P is (-1.32445,0.591751,1)
Two lines through P are L = [6.51303,12.8875,1]
on here!?
All these problems stem from the same underlying cause: roundoff Rememberthat adoublevariable is a rational approximation to a real number Two real quanti-ties that are computed differently may result indoublevalues that are different Forexample, consider this code:
cout << "They are different" << endl;
cout << "Difference = " << x-y << endl;
The bad news is we need to rewrite some of our code to correct this problem Thegreat news is that we only need to repairPObject The childrenPPointandPLineinherit the improvements
Trang 18To begin, let us identify the places in the code forPObjectwhere exact equality
is sought
• Theequalsmethod requires exact equality of the three coordinates
• Theincidentmethod requires zero to be the exact result of thedotproductmethod
• Thedependentprocedure requires zero to be the exact value of the nant
determi-In lieu of exact equality, we can require that the values be within a small tolerance.What tolerance should we use? We make that quantity user selectable initialized withsome default value (say 10−12)
To implement this idea we add a private staticdoublevariable namedtoleranceand define a constant nameddefault_toleranceset to 10−12
InsidePObjectwe define two inline public static methods:set_toleranceandget_tolerance Here is the revised header file
Program 10.11: Header file for the PObject class (version 2)
13 double dot(const PObject& that) const;
14 static double tolerance;
15
16 protected:
17 bool equals(const PObject& that) const;
18 bool less(const PObject& that) const;
19 bool incident(const PObject& that) const;
20 PObject rand_perp() const;
21 PObject op(const PObject& that) const;
Trang 1944 double getX() const { return x; }
45 double getY() const { return y; }
46 double getZ() const { return z; }
47
48 bool is_invalid() const {
49 return (x==0.) && (y==0.) && (z==0.);
Program 10.12: Program file for the PObject class (version 2)
Trang 2021 }
22
23 double PObject::dot(const PObject& that) const {
24 return x*that.x + y*that.y + z*that.z;
36 bool PObject::equals(const PObject& that) const {
37 double d = abs(x-that.x) + abs(y-that.y) + abs(z-that.z);
38
39 return d <= tolerance;
40 }
41
42 bool PObject::less(const PObject& that) const {
43 if (x < that.x) return true;
44 if (x > that.x) return false;
45 if (y < that.y) return true;
46 if (y > that.y) return false;
47 if (z < that.z) return true;
48 return false;
49 }
50
51 bool PObject::incident(const PObject& that) const {
52 return abs(dot(that)) <= tolerance;
Trang 2177 double det = a1*b2*c3 + a2*b3*c1 + a3*b1*c2
78 - a3*b2*c1 - a1*b3*c2 - a2*b1*c3;
79
80 return abs(det) <= PObject::get_tolerance();
81 }
82
83 PObject PObject::rand_perp() const {
84 if (is_invalid()) return PObject(0,0,0);
85
86 double x1,y1,z1; // One vector orthogonal to (x,y,z)
87 double x2,y2,z2; // Another orthogonal to (x,y,z) and (x1,y1,z1) 88
89 if (z == 0.) { // If z==0, take (0,0,1) for (x1,y1,y2)
Trang 22147 double c1 = y*that.z - z*that.y;
148 double c2 = z*that.x - x*that.z;
149 double c3 = x*that.y - y*that.x;
The random point P is (-0.479902,-0.616199,1)
Two lines through P are L = [0.384191,1.32364,1]
previ-Finally, the method we use for testing near equality can be improved For example,
we check if two projective objects are equal by computing their L1 distance andcomparing againsttolerance Alternatively, to checkx1,y1,z1 and x2,y2,z2
for equality, we might consider a test such as this:
|x1− x2| + |y1− y2| + |z1− z2|
|x1| + |x2| + |y1| + |y2| + |z1| + |z2| ≤ε.
Whatever equality test you feel is most appropriate, it is only necessary to edit onemethod (equalsinPObject) to implement your choice
Trang 2310.9 Pappus revisited
We close this section with a program to illustrate Pappus’s Theorems and the use
of the near-equality testing
Program 10.13: A program to illustrate Pappus’s theorem and its dual
22 cout << "Three points on the first line are " << endl
23 << P1 << endl << P2 << endl << P3 << endl;
30 cout << "Three points on the second line are " << endl
31 << Q1 << endl << Q2 << endl << Q3 << endl;
38 cout << "The three points constructed are " << endl;
39 cout << X1 << endl << X2 << endl << X3 << endl;
Trang 2463 cout << "The three lines through the first point are " << endl
64 << L1 << endl << L2 << endl << L3 << endl;
71 cout << "The three lines through the second point are " << endl
72 << M1 << endl << M2 << endl << M3 << endl;
79 cout << "The three lines constructed are " << endl;
80 cout << X1 << endl << X2 << endl << X3 << endl;
Trang 25Enter desired tolerance > 0
You set the tolerance to 0
The two lines are
TROUBLE! The three points are not collinear!!
The two points are
Enter desired tolerance > 1e-16
You set the tolerance to 1e-16
The two lines are
Trang 26(0.160804,-3.85329,1)
(-0.310677,-1.18769,1)
They are collinear, as guaranteed by Pappus’s theorem
The two points are
Enter desired tolerance > 1e-12
You set the tolerance to 1e-12
The two lines are
They are collinear, as guaranteed by Pappus’s theorem
The two points are