Only Bobby can make a point out of circles and teach us something in the process.
Copyright © 2001 Robert H. Schmidt
Here is more actual mail from actual readers who stretch the definitions of Standard C and Standard C++ or confuse my column with Usenet. Names have been changed, and questions surgically altered, to protect the guilty:
- Can you help me to enable the soft key to enter text in a J2ME (KVM/CLDC) application using spotlet?
- I am having trouble getting 16-bit or 32-bit color graphics mode to work in C and DOS.
- Where can I find example C/C++ code thats comparable to code for PLC (Programmable Logic Controllers) ladder logic?
and my perennial favorite
- Id like some kind of immediate response from you for the completion of my project.
Folks, Im sure these are worthy questions well, maybe not the last one but my column is not the proper forum for them. My domain is Standard C and Standard C++: the languages, the library, the standards. I will occasionally dip into some non-Standard or vendor-specific behavior, but only occasionally.
Circular Breathing
Q
Bobby, I have an interesting C++ question.
Why will the following code not compile correctly? I am using Visual C++ v6.0:
class Z; // This line gets // "use of undefined type 'Z'" // if uncommented // //class Z::Y; template<class T> class X { // This next line gets // "use of undefined type 'Z'" T::Y y; }; class Z : public X<Z> { class Y { }; };I have tried several different rearrangements, with little luck. Is it even possible to create a template class that has a member variable of a nested type from the template parameter? Please help!
Thanks Ryan Cunningham
A
Following my usual habit when diagnosing template tantrums, Ive run your sample through EDGs C++ front end. The first error message it returns is
nontype "T::Y" is not a type name T::Y y; ^You can fix this by changing
T::y y;to
typename T::Y y;thereby telling the compiler that T::Y will always instantiate to a type name.
But in the end this wont help much, since the second error, which is now
incomplete type is not allowed typename T::Y y; ^detected during instantiation of
class "X<T> [with T=Z]"has no fix. The trouble ultimately comes from a circular reference to an incomplete type.
At the declaration of
class Z : public X<Z>the direct base class X<Z> must be a complete type. X<Z> is also a specialization of class template X. These facts combine in C++ Standard subclause 14.7.1/4:
A class template specialization is implicitly instantiated if the class type is used in a context that requires a completely-defined object type...
By virtue of appearing in such a context, X<Z> is instantiated at the point of
class Z : public X<Z> // ^^^^ // X<Z> instantiated hereDuring that instantiation, the T in
template<class T> class X { typename T::Y y; };is substituted with template argument Z, so that the class instantiates as
class X<Z> { typename Z::Y y; };Problem is, at the point of this instantiation, the name Z::Y has not yet been declared. Remember, you have just started defining Z and have not yet declared any of Zs members. (You tried getting around this by forward-declaring Z::Y, to no avail [1].)
Bottom line: the act of starting Zs definition requires that Zs definition already have started, a distortion of the time-space continuum beyond the C++ Standards tolerance.
The Long and Winding Road
Q
Hi Bobby,
Please consider the code below. I cannot figure out why I cannot successfully create the expression c == p. Shouldnt the friend functions in class circle be sufficient to support this? Im compiling with Microsoft Visual C++ v6.0.
A limitation is that I cannot modify struct point.
Thanks in advance for your time. Leslie Williamson
A
Your sample code stirs up so many introductory class-design issues that in violation of my usual creed Im devoting most of this months column to it.
My version of your example appears as Listing 1. There youll see two data types:
- a simple C-like struct point, with coordinates x and y of type double.
- a more complex class circle, with center_ of type point and radius_ of type double. A circle constructed with no explicit radius defaults to a unit circle (radius_ = 1).
(point corresponds to what you call A, while circle is what you call B. Ive changed the names and added the radius to make the example more tangible.)
The code defines four equality operators comparing circles to circles and points to circles:
operator==(point const &, circle const &); operator!=(point const &, circle const &); circle::operator==(circle const &) const; circle::operator!=(circle const &) const;And main contains test cases to exercise these operators:
point p = {3, 2}; circle c(3, 2); TEST(p == c); TEST(p != c); TEST(c == p); TEST(c != p);You expect the run-time result of those tests to be
p == c true p != c false c == p true c != p falseBut what you actually get is
p == c false p != c true c == p false c != p trueThe four tests compare different types. If you add tests that compare like types:
TEST(p == p); TEST(p != p); TEST(c == c); TEST(c != c);and rerun the combination, you get
p == c false p != c true c == p false c != p true p == p false p != p true c == c true c != c falseThe only tests that pass are the last two, comparing circle to circle; any comparison involving points fails.
All comparisons directly or indirectly call
circle::operator==(circle const &) const; circle::operator!=(circle const &) const;In the failing comparisons, every point operand must convert to a circle; but in the successful comparisons, no such conversions occur, as both operands are already circles. This pattern suggests that the point-to-circle conversion constructor may be the common failure point.
Youve written that constructor in terms of another constructor. On the surface this makes sense as a code reuse mechanism. Unfortunately the idea goes awry, for your implementation
circle(point const ¢er, double const radius = 1) { this->circle::circle (center.x, center.y, radius); }ends up constructing (and throwing away) a temporary circle object. The state of *this is untouched, as if you had written
circle(point const ¢er, double const radius = 1) { }Net result: circles constructed from points are left uninitialized, and thus dont compare as expected.
I have two simple fixes.
The first fix
circle(point const ¢er, double const radius = 1) { new(this) circle(center.x, center.y, radius); }features a placement new expression that constructs the state of *this in place [2]. I suspect this is what you wanted to do in the first place.
The second fix
circle(point const ¢er, double const radius = 1) { center_.x = center.x; center_.y = center.y; radius_ = radius; }matches the form of your other constructor
circle(double const x, double const y, double const radius = 1) { center_.x = x; center_.y = y; radius_ = radius; }If you substitute either of these fixes for your original, then all eight test cases return desired results.
Now for some other design considerations beyond your bug...
Consideration #1: You have
operator== (point const &, circle const &) operator!= (point const &, circle const &)declared as friends of circle, even though the operators dont access circles private implementation and state. You therefore can declare the classes as non-friends.
Consideration #2: You need those former friends for cases where a point is the left-hand side of an equality expression. You can get around this need by declaring the current members
circle::operator== (circle const &) const; circle::operator!= (circle const &) const;as friends
friend operator== (circle const &, circle const &); friend operator!= (circle const &, circle const &);and eliminating the former friends
operator==(point const &, circle const &) operator!=(point const &, circle const &)Any point passed to these operators, as either the left or right operand, will be converted to a circle via the now-familiar conversion constructor.
Consideration #3: The conversion operator
circle::operator point const &() constmaps the other way, distilling a circle down to a point.
Converting a circle to a point is a dubious proposition, since a single point cant capture the full state of a circle. A circle contains a point and can be initialized from a point, but a circle is not a point.
An arguably better design is a non-operator accessor, such as
class circle { // ... point const &get_center() const { return center_; } // ... };Consideration #4: As a stylistic rule, favor member initializers over member assignment in constructors. In the case of circle, the only member you can initialize is radius_:
circle(double const x, double const y, double const radius = 1) : radius_(radius) { center_.x = x; center_.y = y; }You may be tempted to write
circle(double const x, double const y, double const radius = 1) : center_.x(x), center_.y(y), radius_(radius) { }which wont work. Neither will
circle(double const x, double const y, double const radius = 1) : center_(x, y), radius_(radius) { }Consideration #5: This last form would work if point had an appropriate constructor. Indeed, circle could be tightened overall if point had member functions, for many of circles design decisions properly belong in point. Because you say you cant modify point, you should strongly consider the next best thing: thinly wrap point in a class, push the point-specific design decisions into that class, allow the class to freely convert to/from point, and traffic in that class wherever you can.
I show one possible primitive definition (which I call POINT) in Listing 2. I leave this as an Exercise For The Reader: implement POINT, rewrite circle to take advantage of POINT, and then write test cases to validate your rewrite.
Consideration #6: Unlike circle, the new class POINT conceptually is a point; indeed, POINT is what point would be if you were allowed to modify it. POINT is less an abstraction and more an encapsulation, a way to semantically bundle functions with a C-like struct. As POINTs abstraction layer is purposely quite thin, having it convert freely to a point does not hamper the design.
Given that reality, consider rewriting POINT so that it derives from point instead of containing point. I leave as another Exercise the full exploration of such a rewrites benefits and costs.
Rock and Roll, Part 2
Q
Can you recommend a C++ beautifier? Rex Cramer
A
Oh sure: Visual Studio .NETs csc.exe [3].
Erratica
Speaking of .NET, Visual C++ .NET Beta 2 is available to the world. In my December 2000 column, I promised Id wait until the final version ships before switching to it. However, events in my Microsoft life have forced my hand, and Im now running Visual C++ .NET in lieu of Visual C++ v6.0. In fact, Ive completely wiped the latter from the Virtual PC environment on my PowerBook.
This means I can no longer test your code on earlier versions of Microsofts compiler. From this day forward, if you have a problem that occurs on Visual C++ v6.0, and I cant reproduce it on my system, Ill assume the problem is specific to your compiler.
Editorially, I will call the compiler Visual C++ .NET, to reinforce which version Im using. I wont drop the explicit .NET designation until after the product ships.
On a completely unrelated note...
Diligent Reader Scott Meyers gets way too many jollies from dissing my writing. He clearly needs a new hobby. This time he takes issue with my interpretation of the Standard's intent regarding extern "C". Sadly, as much as I don't want to encourage him, I'm slowly starting to agree with his logic. (Oh cruel, cruel fate!) We should have a verdict by next month.
Notes
[1] I touched on this same issue in my August 2001 column item Ins and Outs, Part 1.
[2] You must include the Standard C++ library header <new> for this to work.
Although Bobby Schmidt makes most of his living as a writer and content strategist for the Microsoft Developer Network (MSDN), he runs only Apple Macintoshes at home. In previous career incarnations, Bobby has been a pool hall operator, radio DJ, private investigator, and astronomer. You may summon him on the Internet via BobbySchmidt@mac.com.