Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

No Brain, No Gain


No Brain, No Gain

Learn how not to declare template friends and implement assignment operators, lest you actually make these mistakes.

Copyright © 2003 Robert H. Schmidt

Thanks to a wildcat strike by most lobes in my brain's right hemisphere, I have no creative preamble this month. My cerebrum and I are in contract negotiations at this moment and hope to have a deal struck soon. Until then I'm replacing my wildcat brain with my domestic cats' brains [1].

The feline writing technique is subtle and deft, yet passionate and creative. As I watch them work, I know I'm in the presence of truly gifted minds. Here's an actual untouched sample of my stale original:

Working

and the cats' incomparable enhancement:

Wor""""""""""""""""""""""""""""""""""||||||\\\\'?
|{{{^66,5pssssssssssssssssssssssssssssss
------------------------------------------------
------------------------------------------------
------------------------------------------------
------------------------------------------------
------------------------------------------------
-----------------------
0ppppppppppppppppppppppppppppppppppppppppppppppp
pppppppppppppppppppppppppppppppppppppppppppppppp
pppppppppppppppppppppppppppppppppppppppppppppppp
pppppppppppppppppppppppppppppppppppppppppppppppp
pppppppppppppppppppppppppppppppppppppppppppppppp
pppppppppppppppppppppppppppppppppppppppppppppppp
pppppppppppppppppppppppppppppppppppppppppppppppp
pppppppppppppppppppppppppppppppppppppppppppppppp
pppppppppppppppppppppppppppppppppppppppppppppppp
pppppppppppppppppppppppppppppppppppppppppppppppp
pppppppppppppppppppppppppppppppppppppppppppppppp
pppppppppppppppppppppppppppppppppppppppppppppppp
ppppppppppppppppppppppp[[[[[[[[[[000000---------
------97tg
My meager thoughts pale beside their tttthouuuughts. Like Lazarus Long, the cats clearly believe that moderation is for monks.

Most incredibly of all, this talent manifests through "random" walks on the keyboard. Random indeed! I can only surmise that this appearance of randomness is a clever form of obfuscation, to keep other lesser talents from stealing their technique.

Smarter Than the Average Pointer

Q Hi Bobby,

There is a discussion at our company about whether it is beneficial to use auto_ptr. The opinion against it is that:

  • Pulling in the header file <memory> slows down compiling.
  • If we watch for small problems associated with using bald pointers, such as memory leaks, references to invalid pointers could be avoided without using auto_ptr.
I would appreciate your opinion on this subject.

Regards,

-- Bao Tran

A  Yes, there are reasons to avoid auto_ptr. But I've never before had anyone suggest that compilation time is one of them.

All else held equal, including a header will usually degrade compilation speed -- so trivially the allegation against #include <memory> is correct. However, the important question to ask is: does the degradation make a significant difference?

Have you or your team done benchmarks to determine just how much drag #include <memory> puts on your builds? I'm guessing not. Unless you are building on glacial hardware, have zillions of source files, or rebuild frequently, I can't imagine that #include <memory> will significantly compromise your build times [2].

As to the second allegation: sorry, but that argument is tantamount to saying "if we avoid all of the problems associated with real arrays, then we don't need vectors." Well yeah, sure; but if those problems were so easily avoided, or if containers and smart pointers existed purely to solve those problems, then nobody would need them.

There are reasons beyond memory-leak prevention for using smart pointers. Among them:

  • Smart pointers can track and profile their own usage patterns.
  • Smart pointers can selectively enable or disable pointer-like behavior.
  • Smart pointers can selectively enable or disable usage with certain types.
  • Smart pointers can allow allocation/deallocation schemes beyond the usual new and delete.
auto_ptr can't do any of these things, but other smart-pointer types can. For a most extreme example of such smart pointers -- no, eccentric genius pointers -- see chapter 7 of Andrei Alexandrescu's book Modern C++ Design (Addison-Wesley, 2001). His publisher has made that chapter available free online [3].

In a narrow sense, I agree with your colleagues that you should avoid auto_ptr, but not for the reasons you give. You should avoid it mostly because it makes some limiting assumptions and has one trick feature (ownership semantics) that is more liability than asset [4].

At the same time, I think you would be remiss if you abandoned all smart-pointer usage. I would actually encourage the opposite tack: that you keep raw-pointer usage to a minimum, mostly for interfacing with C and C-like code, for those few places truly requiring highest performance, or for the lowest-level implementation of smart pointers and similar objects.

No Friend of Mine

Q Hi Bobby,

Why won't the following code compile in C++?

template<typename T>
class A
    {
    friend class T;
    };
My compiler complains:

template parameter T may not be used in an elaborated type specifier
What exactly is an elaborated type specifier and why can't T be used in one? T is well known at compile time, so why can't the compiler perform the (expected?) substitution?

Thanks for your time!
-- Shane Neph

A  Elaborated type specifier. Say it with me: elaborated type specifier. Ahhh, the way it rolls off the tongue. Exquisite. Poetic.

Reverie over.

Here's the grammatical definition, hot from the C++ Standard griddle:

elaborated-type-specifier:
class-key ::opt nested-name-specifieropt identifier
enum ::opt nested-name-specifieropt identifier
typename ::opt nested-name-specifier identifier
typename ::opt nested-name-specifier templateopt template-id

O-kay...

Here's a simple and imprecise definition: a type name prefixed or "elaborated" with the type's kind (class, enum, and so on).

Some examples:

class T
class ::T
class X::T
class ::X::T
struct T
enum T
typename T
typename template T
In your case the elaborated type specifier is:

class T
in the context of:

friend class T;
Your compiler complains that you can't use the template parameter T in this context. You counter that the compiler is being too picky, and that it should know that T is perfectly okay in this context.

I don't know if your counter is true or not, since I don't have a complete example. It doesn't really matter: the compiler is correct, in that it's following the language rules. Indeed, just after the grammatical definition I show above, the Standard has this lovely passage:

If the identifier resolves to a typedef-name or a template type-parameter, the elaborated-type-specifier is ill-formed.

In your example, the identifier (T) resolves to a template type parameter. That fact renders the elaborated type specifier (class T) invalid.

As if reading your mind, the passage's authors continue:

Note: this implies that, within a class template with a template type-parameter T, the declaration

      friend class T;
is ill-formed.

Déjà Vu!

Why are the rules constructed this way? Consider:

template<typename T>
class A
    {
    friend class T;
    };

class X
    {
    // whatever
    };

A<X> x;
You're thinking this last line should work, since the compiler knows that X is a class and can therefore be made a friend of A<X>. But now consider:

A<int> x2;
For this to work the way you want, A<int> would instantiate as:

class A<int>
    {
    friend class int;
    };
I'm sure you see the problem: int can't be a friend of anyone in any context.

One easy solution to your problem is to remove the friend declaration from the primary declaration and move it selectively into specializations. A simple example:

class X
    {
    };

//
// primary declaration of A
//
template<typename T>
class A
    {
    };

//
// specialization of A<X>
//
template<>
class A<X>
    {
    friend class X;
    };
You could make the argument that this grammar limitation is arbitrary, since the language rules allow other template constructions that make assumptions about the type parameter. The example:

template<typename T>
class A2
    {
    typename T::X x;
    };
assumes that T is something that can have members at all, has a particular member named X, and that X is a type. That seems a much more ambitious assumption than does class T; yet the A2 declaration is allowed while the A declaration is not [5].

Refuse to Reuse

Q Often times when writing classes I find that the bulk of the code in the assignment operator is just code from the destructor followed by code from the copy constructor. When actually trying to put that observation into actual code, however, I can only do it partially, and even that doesn't seem right. The included code illustrates all of the things I've tried with their results and error messages. (I'm using Visual C++ .NET.)

Questions:

  • Why does X(x) put an anonymous x instance on the stack?
  • Why doesn't (this->*...)(...) work?
  • Why does (this->...)(...) work?
Any illumination you could provide on these questions would be deeply appreciated.

-- Daniel Mathews

A  My slightly edited version of your sample appears as Listing 1. I've attached a number (#1 through #10) to each test case in your code. I'll target my answers to those numbered cases. After that I'll make some general observations.

Case #1: You can call the destructor explicitly (as I discuss for Case #8), but not by its simple unqualified name ~X(). The compiler interprets that expression as ~ X(), where ~ is the unary complement operator. As the compiler error suggests, you cannot take the unary complement of an X object, because type X doesn't define operator~ and doesn't convert to some other type that allows the built-in ~ operator.

Case #2: In one of those potentially ambiguous grammar points, X(x) could be either an expression creating a temporary X initialized from x, or the declaration of an X object named x. According to the tie-breaking rules, it's a declaration of x. If you immediately follow this line with another X(x), your compiler will validate my claim, by flagging x as multiply declared. If you really want to construct a temporary X from x, use the cast notation (X) x.

Case #3: You say this works. It shouldn't. The ->* operator requires a right operand of type "pointer to member of X." The expression f is not a pointer to an X member and does not convert to a pointer to an X member. If you insist on the pointer-to-member notation, try the truly unpleasant (this->*&X::f)().

Cases #4 and #5: Nope, you can't call constructors and destructors this way. Your error messages refer to the appearance of the type name X in the offending expressions.

Case #6: This shouldn't work. See Case #3.

Case #7: Finally! Something that compiles and actually should compile. In this context, the expression this->f is equivalent to the much simpler f.

Case #8: This is the correct pattern for explicitly calling the X destructor. If you want to be pedantic, you can use the fully qualified form this->X::~X. Sometimes you need this second form: if ~X were a virtual destructor called from a class derived from X, then this->~X() would (virtually) call the derived-class destructor, while this->X::~X() would (statically) call the X destructor [5].

Case #9: This is the correct pattern for invoking the assignment operator as a regular function call rather than as an infix operator. In this context, you can shorten this->operator= to operator=.

Case #10: This fails, as it should. Unlike destructors, constructors cannot be called explicitly. Ultimately this is the capability you really want.

Now for my general observations. The realization you describe is ancient, one that just about all of us run into eventually. Barring technicalities of object identity and lifetime, a copy assignment operation is tantamount to an in-place copy construction over the foundation of a just-destructed object. Since the in-place "copy construction" into old memory is very often the same operation as a genuine copy construction into new memory, you reasonably want to reuse the same code for both.

But as you discovered, you can't call a constructor directly. The closest you can easily come is to pull the common code into a normal (non-constructor) function and call that from both your copy constructor and copy-assignment operator:

class X
    {
public:
    X(X const &x)
        {
        construct(x);
        }
    X &operator=(X const &x)
        {
        if (&x != this)
            {
            this->~X();
            construct(x);
            }
        return *this;
        }
private:
    void construct(X const &)
        {
        // ...
        }
    };
This pattern, while common, is not without limitation:

  • const members must have their values set in a constructor's member initializer list. Once set, const members cannot be changed without a mild hack.
  • Reference members must be bound to their referenced objects in a constructor's member initializer list. Once bound, reference members cannot be rebound to other objects without a severe hack [7].
  • In the copy constructor, each member is initialized (either explicitly in an initialization list, or implicitly by default construction), and then effectively "reinitialized" in construct. At best this redundancy may cause inefficiency; at worst it may distort your program's semantics.
  • Inheritance complicates the pattern. For an example, see Case #8 above. Also consider what happens if operator= is virtual in X or X's base.
I'll also note that the simple pattern of testing for self-assignment:

if (&x != this)
does not work in the general case, especially when complicated inheritance hierarchies are involved. In addition, as Herb Sutter explains [8], exception-safe code typically doesn't require this check. Even if you don't particularly care about exception safety, I suggest you read Herb's piece anyway, as it shows an alternative (and now commonly accepted) approach to copy assignment.

Notes

[1] Oh, alright, since someone will ask: the cats names are John Lodge and Artemis, where John Lodge = the Moody Blues' bassist, and Artemis = the Greek goddess of the hunt. They are twins, and I've had them pretty much since they were born. They will have their 15th birthday just as this column hits the streets.

[2] I'm talking here strictly of header-inclusion time, not template-instantiation time. If you reference auto_ptr all over the place for lots of different types, the instantiation times for all of those auto_ptr specializations could add up.

[3] <www.aw.com/samplechapter/ 0201704315.pdf>

[4] Now "avoid" does not mean "never use," for there are times that auto_ptr is useful and correct. If you never assign them, restrict them to fairly bounded scopes, and use them just to guarantee object deletion, auto_ptrs can be reasonable tools. (Chuck Allison tells me he uses them, so you know they can't be all bad.)

[5] Yes, yes, yes, I know that if you try instantiating A with a type that doesn't have a proper X member, the compilation will fail. But that failure occurs at the point of A instantiation; if you never instantiate A, the compilation can succeed. The example using friend class T will always fail at the point of A declaration, even if you don't actually instantiate A.

[6] Destructors alter the usual virtualization rules. Normally if you call a function virtually, you land in some function with the same name. But if you call a destructor virtually, you can land in a function with a different name. For example, if D is derived from B, a virtual call to ~B from a D object would land in ~D -- a differently-named function.

[7] You can change the value of the referenced object, but not the actual reference itself -- a subtle but real distinction.

[8] < www.gotw.ca/gotw/059.htm>

About the Author

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 [email protected].


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.