C/C++ Contributing Editors


Uncaught Exceptions: Standard Languages: Ask for Them by Name!

Bobby Schmidt

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:

and my perennial favorite

Folks, I’m 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, I’ve run your sample through EDG’s 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 won’t 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 here

During 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 Z’s members. (You tried getting around this by forward-declaring Z::Y, to no avail [1].)

Bottom line: the act of starting Z’s definition requires that Z’s definition already have started, a distortion of the time-space continuum beyond the C++ Standard’s 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. Shouldn’t the friend functions in class circle be sufficient to support this? I’m 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 — I’m devoting most of this month’s column to it.

My version of your example appears as Listing 1. There you’ll see two data types:

(point corresponds to what you call A, while circle is what you call B. I’ve 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   false

But what you actually get is

p == c   false
p != c   true
c == p   false
c != p   true

The 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   false

The 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.

You’ve 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 &center,
        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 &center,
        double const radius = 1)
    {
    }

Net result: circles constructed from points are left uninitialized, and thus don’t compare as expected.

I have two simple fixes.

The first fix

circle(point const &center,
        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 &center,
        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 don’t access circle’s 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 &() const

maps the other way, distilling a circle down to a point.

Converting a circle to a point is a dubious proposition, since a single point can’t 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 won’t 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 circle’s design decisions properly belong in point. Because you say you can’t 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 POINT’s 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 rewrite’s benefits and costs.

Rock and Roll, Part 2

Q

Can you recommend a C++ beautifier? — Rex Cramer

A

Oh sure: Visual Studio .NET’s csc.exe [3].

Erratica

Speaking of .NET, Visual C++ .NET Beta 2 is available to the world. In my December 2000 column, I promised I’d wait until the final version ships before switching to it. However, events in my Microsoft life have forced my hand, and I’m now running Visual C++ .NET in lieu of Visual C++ v6.0. In fact, I’ve 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 Microsoft’s compiler. From this day forward, if you have a problem that occurs on Visual C++ v6.0, and I can’t reproduce it on my system, I’ll assume the problem is specific to your compiler.

Editorially, I will call the compiler Visual C++ .NET, to reinforce which version I’m using. I won’t 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.

[3] I’m teasing. I think.

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.