Bobby finds still more to warn about when playing fast and loose with pointers, smart pointers, and arrays.
Copyright © 2000 Robert H. Schmidt
As I write, my mother's birthday is in one week. Several of you have expressed condolences to me for her passing. To those who contacted me and to those who didn't, but kept me in their thoughts and prayers I give my deepest thanks.
Exceptions-R-Us
Q
Hi Bobby,
I do enjoy reading your columns a lot. Through the Q&A I have been able to learn a lot. Thank you very much.
I have three questions regarding your January item about exceptions. I appreciate you taking a look at these questions.
1) Can I use cout instead of printf?
2) Are the semicolons I've indicated a typo or are they part of the syntax?
3) Can we specify multiple catch(...) statements or should that be only specified once and in the end of all other catches?
Thank you so much in advance and have an excellent day.
Sincerely Visda Vokhshoori
A
Short answers:
1) Usually yes.
2) Yes to both.
3) Yes (but don't do it) and yes.
Long answers:
The item in question was titled "Deconstruction" and involved construction/destruction of temporary exception objects by Microsoft Visual C++. The collateral source code appeared as Listing 1 in that January 2000 column. For the purposes of your questions, the relevant parts of that source are:
#include <stdio.h> class error { public: error() { printf("error()\n"); }; // <== semicolon ~error() { printf("~error()\n"); }; // <== semicolon }; int main() { try { } catch (...) { }; return 0; }Now to your questions...
Answer #1: You can indeed use cout instead of printf. I use printf in many published samples for several reasons:
- I test every code piece that I publish. My edit/compile/run cycle is shorter when I use <stdio.h> instead of <iostream>.
- Although <iostream> declarations are supposed to live in namespace std, not all compilers properly support that namespace. <stdio.h> is a C library header, and thus avoids any std-related difficulties.
- One of the standard library implementations I use calls ::operator new internally before fully initializing the <iostream> plumbing. When I create a replacement ::operator new, and that replacement references cout to trace calls, my programs end unpleasantly. A printf-traced version of the same ::operator new works fine.
Answer #2: I've flagged the two questionable semicolons with comments. They are typos (in that I didn't intend them) but not invalid. The C++ grammar allows an optional semicolon following a function definition assuming the definition appears within a class definition [1]. However, if you move that same function definition outside the class definition:
class error { // ... }; error::error() { }; // errorthe extra semicolon induces an error with conforming compilers. (On my system, Microsoft and Metrowerks accept the code, while EDG flags the error.)
Answer #3: Handlers are matched in top-down order. Since the catch (...) handler matches all exception types, you should write it last in the handler sequence. Otherwise, the handlers after catch (...) will never be entered.
The language rules do let you define multiple handlers with the same exception declaration:
try { } catch (int) { } catch (int) { // ... never entered } catch (...) { } catch (...) { // ... never entered }That you can write such handlers doesn't mean that you should. Remember, handlers are matched from the top down, meaning that the first handler in each repeated pair above masks the second handler in that pair.
Autoamerican
Q
Is there a version of the auto_ptr class in the C++ Standard that works with a pointer to an array? For example:
auto_ptr<int> pNumbers(new int[3]);doesn't work right since the auto_ptr<> destructor calls delete instead of delete [].
Thanks Abu Wawda
A
(I assume that where you write "a pointer to an array" you really mean "an array" or "a pointer to an array's first element.")
You can't manage arrays with auto_ptr, for the very reason you cite: the auto_ptr destructor assumes that its managed object is 1) created from the free store, and 2) not an array.
Fortunately, you have two fairly easy alternatives:
- Create your own auto_ptr-like class that manages arrays [2].
- Use an array-like object (such as a vector) instead of a real array, and have auto_ptr manage that.
This "feature" of auto_ptr is yet one more reason to prefer vectors and strings over arrays. In my own work, I almost never use arrays anymore, unless I'm calling into or (less often) providing a C-style interface [3].
Something for Nothing
Q
Hi Bobby.
I guess this is a simple question for you:
class A { }; class B { char b; };Why do sizeof(A) and sizeof(B) equal 1? In particular, where is the one byte in class A?
Thanks and regards Dmitry Khaliapin
A
Time to haul out the Standard again. From 5.3.3 ("Sizeof"):
sizeof(char), sizeof(signed char) and sizeof(unsigned char) are 1.
And from 1.8 ("The C++ object model"):
If a complete object, a data member, or an array element is of class type, its type is considered the most derived class, to distinguish it from the class type of any base class subobject; an object of a most derived class type is called a most derived object.
Unless it is a bit-field, a most derived object shall have a non-zero size and shall occupy one or more bytes of storage.
Both A and B are most derived classes they are not bases of some other class. The size of such classes is always >= 1. Thus, sizeof(A) and sizeof(B) will both be >= 1. On your compiler, the size of each is exactly 1, meaning that A and B objects require no alignment padding [4].
If you use A and B as base classes, so that they are no longer "most derived," the results change. Again from subclause 1.8:
Base class sub-objects may have zero size.
That "may have" assumes the base class is otherwise empty. Since A has no data members, an A base subobject may have zero size. To test this, try
class A1 : A { char a1; }; cout << sizeof(A1) << endl;If the result is 1, the A subobject has size 0.
If B is a base class, the result is different:
class B1 : B { char b1; }; cout << sizeof(B1) << endl;This should show sizeof(B1) to be >= 2, with one byte coming from the B base sub-object, and one or more bytes coming from the B1 "most derived" object. Unlike A, B contains data members. Thus the rule allowing zero-size base subobjects does not apply to B it will always have a non-zero size.
This difference leads to a weak motivation for preferring private inheritance over containment: the so-called Empty Base Optimization. Base subobjects can have zero size, while data members always have non-zero size. If you inherit from "empty" classes instead of containing them, your derived-object size can be smaller:
class E1 { typedef char c; }; class E2 { static int i; }; class E3 { void f(); }; class contained { E1 e1; E2 e2; E3 e3; }; class inherited : E1, E2, E3 { };You should find that sizeof(contained) is >= 3, while sizeof(inherited) may be only 1.
While this technique can save space, there are caveats. Compilers are allowed, but not required, to give empty bases zero size; the optimization is therefore not portable. In addition, inheritance brings increased coupling and semantic baggage that containment does not. This cost probably outweighs the space benefit, unless you are creating many such objects or have extremely cramped memory.
FAQ
Q
Love your magazine. I have a question involving pointers to a two-dimensional array that has crossed my path before but has come up again. I am trying to pass a pointer to an array of "strings." The compiler does not seem to like my declarations. Yet if I typedef the string array it works. If it is of any help, I am writing C code on AIX. See the following example. (Keep in mind that this simplified version of my problem is used just to show a point.)
The compiler does not like this snippet of code:
int main() { char **p; char a[5][30]; p = a; // error here return 0; }I get the following error where indicated:
Operation between types "unsigned char**" and "unsigned char(*)[30]" is not allowed.However, the following works fine:
typedef char Str[30]; int main() { Str *p; Str a[5]; p = a; return 0; }What would be the best way to declare this pointer to a two-dimensional array? Thanks.
P.S. You don't have to use this in the magazine. Eugene Wilson
A
Oh, but I do! For this question gives me a chance to remind Diligent Readers of a fundamental C and C++ conversion rule: in many (but not all) contexts, an array:
T a[N];will implicitly convert into a pointer to its first element:
T *p = a; // p points to a[0]as if you had written an explicit conversion:
T *p = (T *) a; // equivalentFollowing the lead of your second example:
typedef char T[30]; T a[5]; T *p = a; p = (T *) a;T is the type "array of 30 chars," a has type "array of five arrays of 30 chars," and p has type "pointer to array of 30 chars."
If I omit the T definition, the code is less readable but equivalent:
char a[5][30]; char (*p)[30] = a; p = (char (*) [30]) a;This is what you should have done with your first example. Instead, you defined p as
char **p = a; // errorwhich is tantamount to
char **p = (char (*)[30]) a;The type char (*)[30] does not convert to char ** hence the error message.
Erratica
I have not touched the Borland compiler in weeks. So much for my bold promises.
Last month I wondered why the language rules forbade handling subobject constructor throws. Now I know: such handling would violate the C++ object model. To see my reasoning in detail, read my mid-February MSDN column at <http://msdn.microsoft.com/library/welcome/dsmsdn/deep02172000.htm>.
Notes
[1] C++ Standard subclause 9.2 ("Class members"), opening grammar.
[2] I covered this subject several years ago. Check out my "Learning C/C++urve" columns from the July 1997 through February 1998 issues of CUJ.
[3] Or even less often: while writing COM code. Because pointers are fungible among languages, and COM is supposed to be language agnostic, COM interfaces like to traffic in pointers.
[4] For more on class object alignment and padding, see my "More Mousetraps" item from last month's column.
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.