|
|
|
Sponsored Link •
|
|
Advertisement
|
Since it's a function like any other, the operator
&() overload can do things other than simply return a
converted value. This has serious consequences.
operator&()
breaks encapsulation.
That's a bold statement. Let me illustrate why it is so.
As I've mentioned already, ATL has a large number of wrapper
classes that overload operator &(). Unfortunately,
there are different semantics to their implementations. The types
shown in Table 26.1 all have an assertion in the operator method
to ensure that the current value is NULL.
| Wrapper Classes | operator&() Return Type |
|---|---|
CComTypeAttr |
TYPEATTR** |
CComVarDesc |
VARDESC** |
CComFuncDesc |
FUNCDESC** |
CComPtr / CComQIPtr |
T** |
CHeapPtr |
T** |
Table 26.1
Don't worry about the specifics of the types
TYPEATTR, VARDESC and
FUNCDESC—they're POD Open type structures (see
Section 4.4) used for manipulating COM meta data. The important
thing to note is that they have allocated resources associated
with them but they do not provide value semantics, which means
that they must be managed carefully in order to prevent resource
leaks or use of dangling pointers.
The operator is overloaded in the wrapper classes to allow
these types to be used with COM API functions that manipulate the
underlying types, and to be thus initialised. Of course, it's not
an initialisation as we RAII-phile C++ types know and love it, but
it is initialisation, because the assertion means that any
subsequent attempt to repeat the process will result in an error,
in debug mode at least. I'll leave it up to you to decide whether
that, in and of itself, is a good way to design wrapper classes,
but you can see that you are required to look inside the library
to see what is going on. After all, it's using an overloaded
operator, not calling a function named
get_one_time_content_pointer()[1].
The widely used CComBSTR class, which wraps the
COM BSTR type, also overloads operator
&() to return BSTR*, but it does not
have an assertion. By contra-implication, we assume that this
means that it's OK to take the address of a CComBSTR
multiple times, and, since the operator is non-const, that we can
make multiple modifying manipulations to the encapsulated
BSTR without ill-effect. Alas, this is not the case.
CComBStr can be made to leak memory with ease:
void SetBSTR(char const *str, BSTR *pbstr);
CComBSTR bstr;
SetBSTR("Doctor", &bstr); // All ok so far
SetBSTR("Proctor", &bstr); // "Doctor" is now lost forever!
We can surmise that the reason CComBSTR does not
assert is that it proved too inconvenient. For example, it is not
uncommon to see in COM an API function or interface method that
will take an array of BSTR. Putting aside the issue
of passing arrays of derived types (see Sections 14.5; 33.4), we
might wish to use our CComBSTR when we're only
passing one string.
An alternative strategy is to release the encapsulated resource
within the operator &() method. This is the approach
of another popular Microsoft COM wrapper class, the Visual C++
_com_ptr_t template. The downside of this approach is
that the wrapper is subject to premature release on those
occasions when you need to pass a pointer to the encapsulated
resource to a function that will merely be using it, rather than
destroying it or removing it from your wrapper. You may think that
you can solve this by declaring const and non-
const overloads of operator &(), as in
Listing 26.2.
template <typename T>
class X
{
. . .
T const *operator &() const
{
return &m_t;
}
T *operator &()
{
Release(m_t);
m_t = T();
return &m_t;
}
Unfortunately, this won't help, because the compiler selects
the overload appropriate to the const-ness of the
instance on which it's to be called, rather than on the use one
might be making of the returned value. Even if you pass the
address of a non-const X<T> instance to a function
that takes T const *, the non-const
overload will be called.
To me, all this stuff is so overwhelmingly nasty that I stopped using any
such classes a long time ago. Now I like to use explicitly named methods
and/or shims to save me from all the uncertainty. For example, I use the
sublimely named[2] BStr
class to wrap BSTR. It provides the
DestructiveAddress() and NonDestructiveAddress()
methods, which, though profoundly ugly, don't leave anyone guessing as to
what's going on.
Another source of abuse in overload operator &() is
in the type it returns. Since we can make it return anything, it's
easy to have it return something bad; naturally, this is the case
for any operator.
We saw in Chapter 14 some of the problems attendant in passing
arrays of inherited types with functions that take pointers to the
base type. There's another dimension to that nasty problem when
overloading operator &(). Consider the following
types:
struct THING
{
int i;
int j;
};
struct Thing
{
THING thing;
int k;
THING *operator &()
{
return &thing;
}
THING const *operator &() const;
};
Now we're in the same position we would be if
Thing inherited publicly from THING.
void func(THING *things, size_t cThings); Thing things[10]; func(&things[0], dimensionof(things)); // Oop!!
By providing the operator &() overloads for
"convenience", we've exposed ourselves to abuse of the
Thing type. I'm not going to suggest the application
of any of the measures described in Chapter 14 here, because I
think overloading operator &() is just a big no-no.
A truly bizarre confluence of factors is the case where the operator is destructive—it releases the resources—and you are passing an array of (even correctly size) wrapper class instances to a function, as in Listing 26.4.
struct ANOTHER
{
. . .
};
void func(ANOTHER *things, size_t cThings);
inline void func(array_proxy<ANOTHER> const &things)
{
func(things.base(), things.size());
}
class Another
{
ANOTHER *operator &()
{
ReleaseAndReset(m_another);
return &m_another;
}
private:
ANOTHER m_another;
};
Let's assume you're on your best behaviour, and are using an
array_proxy (see Section 14.5.5) and translator
method to ensure that ANOTHER and
Another can be used together.
Another things[5]; . . . // Modify things func(things); // sizeof(ANOTHER) must == sizeof(Another)
Irrespective of the semantics of func(), in
calling the function things[0] will be reset and
things[1] - things[4] will not be
affected. This is because the array constructor of
array_proxy uses explicit array subscript syntax, as
all good array manipulation code should. If you were to do it
manually, you'd still need to apply the operator, unless
Another inherited publicly from ANOTHER
and you called the two parameter version of func()
and relied on array decay.
If func() does not change the contents of the
array passed to it, then this supposedly benign call has the nasty
side effect of destroying the first element passed to it. If
func() modifies the contents of the array, then
things[1] - things[4] are subject to
resource leaks, as their contents prior to the call are simply
overwritten by func().
|
Sponsored Links
|