Bobby gets hitched, pines for Oz, and deals with several common confusions in C/C++, all in one action-packed episode.
Copyright © 2000 Robert H. Schmidt
I've had Sydney on the brain lately.
In three days my fiancée Annie [1] leaves for a six-week vacation in Australia. Her mum still lives just outside Sydney; they haven't seen each other in almost a decade. After losing my mother last year, I made it a personal priority for Annie to see hers this year.
We just saw the new Mission Impossible movie for the first time, and The Matrix for the second; both were filmed largely in Sydney. The fringe movie channels on my digital cable system have been showing a rash of Australian movies lately. And God help me, but I've been sneaking peaks at the truly wretched Aussie TV series Skippy on the Discovery Kids' channel.
While Annie's gone, I'll be slaving away at Microsoft. Now that I'm back with the company, I'm working closely with the Visual C++ team. I first met their product manager in Sydney over eight years ago. Several other team members are also from Down Under (although I suspect one is a Kiwi I have trouble discerning the accents).
And as you probably know by now, the 2000 Summer Olympic games will be held in Sydney this September. While we had toyed with the notion of attending those games, circumstances require me to be here this summer. Besides, Annie's dragging her 14-year-old tadpole with her, so they're bound to the school's vacation calendar.
We don't miss everything about Sydneyside living, but we do miss a lot. We talk about having a second home there some day, so we can rejoice in eternal summer. I envy Annie her chance to reconnect with her home, and very much look forward to our first sojourn there together.
[const]_iterator
Q
Re: Q&A topic "Hobson's Choice" from the August 1999 issue
As you say, P.J. Plauger wrote Microsoft's Standard libraries, and they compile cleanly under EDG's -strict switch. But he made a bit of a cock-up of his iterators, at least in Visual C++ v5. If you look in <list>, you will see that he has iterator as a public base to const_iterator! So things like this:
typedef std::list<int> X; void f(X const &x) { X::iterator const it = x.begin(); *it = 7; }work just fine! Lou Lavery
A
P.J. confirms this bug in the version 5 library, saying that the incorrect derivation was an accident rather than a design flaw. He corrects the bug in the version 6 library, deriving iterator from const_iterator as he intends. While I don't know for certain why P.J. implements his library this way, I can make a pretty fair guess.
According to the C++ Standard (Subclause 23.1, Table 65), all Standard library containers are required to define types iterator and const_iterator. Table 65 also specifies that for a given container class X X::iterator "is convertible to X::const_iterator." The Standard does not mandate how the conversion occurs, leaving that detail to the library implementer.
As implementer, Plauger chose to define iterator and const_iterator as distinct class types, with iterator derived from const_iterator. The derivation allows the usual standard conversion from derived (iterator) to base (const_iterator), thereby satisfying the Standard's requirements.
Plauger also declares a const_iterator constructor that accepts an iterator argument. This constructor provides another conversion path from iterator to const_iterator. According to Plauger, the redundancy is a transitional error from one library design to another, and is not intended. He claims (but I cannot verify) that the latest versions of his Dinkumware library do not contain this redundant constructor.
More on const_iterator
Q
I have written a class called range_set, which was featured in the June 1999 issue of CUJ. range_set is designed to be compatible with std::set. I developed this class with Visual C++ v5 SP3, under which it compiled and ran fine. But compiling under Visual C++ v6 has uncovered a "const" problem that was previously undetected.
The problem stems from the fact that std::set::iterator is really a const_iterator (and so is my range_set::iterator), since you can't change the value in a set via an iterator. My implementation of range_set uses std::list internally (which has separate const and non-const iterators). Consequently, range_set::const_iterator has a std::list::const_iterator member.
Now the problem is that my range_set::insert calls std::list::insert passing the std::list::const_iterator as the first parameter. But std::list::insert takes a non-const iterator as its first parameter. This causes the compilation error under Visual C++ v6.
My question is: Why doesn't STL (Standard Template Library) specify that std::list::insert takes a const_iterator instead of a (non-const) iterator? After all, no value in the list is modified via this iterator, and the fact that you are calling a non-const member of std::list (i.e. insert) implies that the list itself may be modified.
My other question is: How do I cast an iterator to a const_iterator? The obvious way would be with a const_cast, but that (and all the other cast operators) give a compilation error (with Visual C++ v6 SP1). Andrew Phillips
A
As your second question segues nicely from reader Laverly's, I'll tackle it first.
In the Table 65 excerpt I quoted above, the Standard specifies that iterator "is convertible to" const_iterator. The Standard does not say if the conversion must be implicit, or if a cast could be required. I'm going to assume the former, that the conversion must occur without a cast.
Under that assumption, you should be able to write
typedef std::list<int> X; X::iterator i; X::const_iterator ci = i;without any casts at all. As I discussed above, the Visual C++ v6 library implements this conversion by deriving iterator from const_iterator. That same library uses a different approach for vectors:
typedef std::vector<int> X; X::iterator i; X::const_iterator ci = i;Instead of creating an inheritance relationship, std::vector defines iterator and const_iterator as int * and int const *, respectively. The conversion from iterator to const_iterator thus maps to the standard conversion from int * to int const *, which requires no cast.
With your other question, you've hit on a hole in the library's const correctness. Citing a similar problem, Dave Abrahams filed a potential Defect Report with the standards committee's Library Working Group (a.k.a. LWG). From his report:
It is the constness of the container which should control whether it can be modified through a member function such as erase, not the constness of the iterators. The iterators only serve to give positioning information.
The proposed resolution: Change all non-const iterator parameters of standard library container member functions to accept const_iterator parameters. Note that this change applies to all library clauses, including strings.
The Working Group considered Dave's issue, but finally decided that it did not represent a defect. Their response (slightly edited):
The issue was discussed at length. It was generally agreed that
- There is no major technical argument against the change (although there is a minor argument that some obscure programs may break).
- Such a change would not break const correctness.
The concerns about making the change were that it
- is user detectable (although only in boundary cases).
- changes a large number of signatures.
- seems more of a design issue that an out-and-out defect.
The LWG believes that this issue should be considered as part of a general review of const issues for the next revision of the standard. Also see issue 200.
In that issue 200, we find this illuminating note:
The LWG believes this is the tip of a larger iceberg; there are multiple const problems with the STL portion of the library and that these should be addressed as a single package.
Bottom line: the committee knows this is broken, but they want to fix it as part of a systemic const-correctness overhaul to the STL.
Array Decay Blues
Q
Revered Sir,
I like the way you try to reason out the possible bugs in the code someone sends you or the way you present them, which is quite easy to comprehend.
I am facing a problem with an ANSI C compiler when I do something like this:
/* file1.c */ int arr[5]; /* file2.c */ extern int *arr;Hope you can spare some of your precious time in helping me out.
Thanks in advance.
Regards Vijayan T. Dorairajan
A
While you don't say so explicitly, I'm assuming that your program compiles fine, but either fails to link or (more likely) links but runs incorrectly.
You've declared a single global entity (arr) with different types in different parts of your program. In file1.c, you define arr as having type "array of 5 ints," while in file2.c you declare arr as having type "pointer to int."
The actual arr object within your executable is an array, and as all Diligent Readers know, an array will often decay to a pointer. However, this is not the same as saying that an array is a pointer. It's this confusion between what an array is, and what an array converts to, that underlies your difficulty [2].
For the sake of discussion, assume that sizeof(int) == sizeof(int *) == 4 in your program. Also assume that file1.c initializes arr as
int arr[5] = {1, 2, 3, 4, 5};If file2.c evaluates the expression
arr[3] = 123;the following occurs:
- file2.c declares arr as an int * and therefore believes that the chunk of storage named arr contains the address of an int object.
- The "pointer" in arr is fetched. Since pointers and ints have the same size, the value fetched is 1.
- The expression arr[3] adds 3 * sizeof(int), or 12, to the "pointer" 1, yielding the "pointer" 13.
- The storage at byte addresses 13 through 16 is set to 123.
Thus, instead of setting the third element of arr to 123, file2.c sets whatever happens to be at address 13 to the value 123. This is almost certainly not correct behavior.
The easiest solution is to declare arr in a header as
extern int a[5];then include that header in both file1.c and file2.c. Assuming you don't otherwise change either file, the compiler will flag the conflicting declaration in file2.c.
Super Trouper
Q
Hi Bobby,
Recently I used a simple inheritance where the child's method uses the parent's implementation:
class Top { public: virtual void f() { // ... } }; class Bottom: public Top { public: virtual void f() // override { Top::f(); // ... } };Later, I put another class Middle between Top and Bottom:
class Middle: public Top { public: virtual void f() // override { Top::f(); // ... } }; class Bottom: public Middle //I changed this... { public: virtual void f() // override { Top::f(); //...but forgot to //change this // ... } };But actually I had in mind:
class Bottom: public Middle { public: virtual void f() { // correct implementation Middle::f(); // ... } };So my question is: Can one somehow specify to use the implementation of the immediate parent without explicitly naming it (like in a directory using "..")? Do you know another way this problem could be avoided? (This problem bit me when there were several different Bottom classes.)
Sincerely Joachim Eibl
A
What you want is the equivalent of Java's super keyword. You aren't alone I've seen this request, or a variation of it, in many C++ newsgroup postings. Unfortunately for you (and those posters), C++ does not support such a keyword.
Java allows only single inheritance of implementation. As a result, a Java derived (or sub) class has at most one direct base (or super) class. Within the derived class, the keyword super references this unambiguous base portion. In contrast, a C++ derived class may inherit implementation from multiple base classes. A super keyword wouldn't unambiguously work in such contexts.
Restricting myself to single inheritance of implementation, I've come up with various ways to approximate super's effects. Although no technique is completely satisfactory, I think a solution using templates is not too heinous.
Consider this variation of your first example:
class Top { public: virtual void f() { // ... } }; template<typename super> class Bottom_ : public super { public: virtual void f() { super::f(); // calls Top::f // ... } }; typedef Bottom_<Top> Bottom;Highlights:
- What was a derived class Bottom in your example is now a class template Bottom_.
- Bottom_ takes a single parameter super. When Bottom_ is instantiated, the type argument bound to super becomes Bottom_'s base class.
- Bottom_ is specialized for exactly one type argument (Top).
- The resulting class Bottom_<Top> is aliased to the type name Bottom.
- Within the context of Bottom, the name super acts as a faux keyword aliased to the base class type Top. (This behavior differs from Java's, which treats super as an object reference, not a type alias.)
My technique scales as you want. If you introduce an intermediate class Middle, you don't need to change Bottom_'s implementation:
class Top { // ... same as before }; template<typename super> class Middle_ : public super // new { public: virtual void f() { // ... } }; typedef Middle_<Top> Middle; // new template<typename super> class Bottom_ : public super { // ... same as before }; typedef Bottom_<Middle> Bottom; // changedThe only required change to existing code is the replacement of
typedef Bottom_<Top> Bottom;with
typedef Bottom_<Middle> Bottom;This change corresponds to the analogous change in your original example from
class Bottom : public Topto
class Bottom : public MiddleBecause Bottom now takes Middle as its base class, the expression super::f within Bottom magically references Middle::f as you desire.
Notes
Bobby Schmidt is a freelance writer, teacher, consultant, and programmer. He is also an alumnus of Microsoft, a speaker at the Software Development and Embedded Systems Conferences, and an original "associate" of (Dan) Saks & Associates. In other 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.