How does dereference operator overloading really work?

J

Joe Seigh

Is there a good write on this. The textbooks I have fluff over
on this? Specifically, I trying to dereference with 2 levels
of type conversion not 1, i.e.

X<T> -> z => Y<T> -> z => T* -> z
and
*X<T> => *Y<T> => *T


The Y<T> conversion has to be done as an expression temp. It cannot be done
inside a method body for X.

I think I sort of had it working for the -> operator but the same
technique doesn't work for the * operator. The * appears to getting
consumed.

Joe Seigh
 
G

Gianni Mariani

Joe said:
Is there a good write on this. The textbooks I have fluff over
on this? Specifically, I trying to dereference with 2 levels
of type conversion not 1, i.e.

X<T> -> z => Y<T> -> z => T* -> z
and
*X<T> => *Y<T> => *T


The Y<T> conversion has to be done as an expression temp. It cannot be done
inside a method body for X.

I think I sort of had it working for the -> operator but the same
technique doesn't work for the * operator. The * appears to getting
consumed.

I don't really get what you're trying to do. Maybe posting a chunk o
code that mostly compiles and show us what you'd like to do ?
 
K

Kevin Goodsell

Joe said:
Is there a good write on this. The textbooks I have fluff over
on this?

Uh... Did somebody swap the '?' and '.' keys on your keyboard?
Specifically, I trying to dereference with 2 levels
of type conversion not 1, i.e.

X<T> -> z => Y<T> -> z => T* -> z
and
*X<T> => *Y<T> => *T


The Y<T> conversion has to be done as an expression temp. It cannot be done
inside a method body for X.

I think I sort of had it working for the -> operator but the same
technique doesn't work for the * operator. The * appears to getting
consumed.

I'm sorry, I have absolutely no idea what you are asking.

-Kevin
 
J

Joe Seigh

Gianni said:
I don't really get what you're trying to do. Maybe posting a chunk o
code that mostly compiles and show us what you'd like to do ?

T * X<T>::eek:perator ->();

gives one level of conversion. I don't want that. Something like


Y<T> X<T>::eek:perator ->();
T * Y<T>::eek:perator ->();


I need a Y<T> temp generated. Strictly speaking, I would think no
overloading of -> should be needed. Type conversion should be sufficient
if I have a conversion path from X<T> to T*.

Joe Seigh
 
R

Rob Williscroft

Joe Seigh wrote in
Is there a good write on this. The textbooks I have fluff over
on this? Specifically, I trying to dereference with 2 levels
of type conversion not 1, i.e.

X<T> -> z => Y<T> -> z => T* -> z
and
*X<T> => *Y<T> => *T


The Y<T> conversion has to be done as an expression temp. It cannot
be done inside a method body for X.

I think I sort of had it working for the -> operator but the same
technique doesn't work for the * operator. The * appears to getting
consumed.

We really need to know what you are trying to do, otherwise
all you can expect is guess, If you havent got any code to show
perhapse give an example of the code whant to write and what you
want it to actually do.

Anyway here's my punt:


#include <iostream>
#include <ostream>

template < typename T > struct thing;


template < typename T >
struct ptr_holder
{
T *temp;

T *operator -> () const { return temp; }

ptr_holder( T *arg ) : temp( arg )
{
thing< T >::aquire( temp );
}

~ptr_holder()
{
thing<T>::release( temp );
}
};

template < typename T >
struct ref_holder
{
T *temp;

operator T &() const { return *temp; }

ref_holder( T *arg ) : temp( arg )
{
thing< T >::aquire( temp );
}

~ref_holder()
{
thing<T>::release( temp );
}
};


template < typename T >
struct thing
{
T *data;

static void aquire( T * data )
{
std::cout << "aquire( " << (void *)data << " )\n";
}
static void release( T * data )
{
std::cout << "release( " << (void *)data << " )\n";
}

ptr_holder< T > operator -> () const
{
return ptr_holder< T >( data );
}

ref_holder< T > operator * () const
{
return ref_holder< T >( data );
}

thing( T & ref ) : data( &ref ) {}

};



void function( int &arg )
{
std::cout << "function( " << arg << " )\n";
}

struct X
{
int y;
X( int zz ) : y( zz ) {}
};

void function( X & x )
{
std::cout << "function( X( " << x.y << " )& )\n";
}

int main()
{
int a = 1;
thing< int > at( a );
function ( *at );

X x( *at );
thing< X > xt( x );
std::cout << "inline: " << xt->y << "\n";
function( *xt );
}


Rob.
 
K

Kevin Goodsell

Joe said:
T * X<T>::eek:perator ->();

gives one level of conversion. I don't want that. Something like


Y<T> X<T>::eek:perator ->();
T * Y<T>::eek:perator ->();


I need a Y<T> temp generated.

Why would you need that? I suppose you could do something like this:

T* X<T>::eek:perator->()
{
return T<T>()->something;
}

But I can't think of any reason to need a temporary.
Strictly speaking, I would think no
overloading of -> should be needed. Type conversion should be sufficient
if I have a conversion path from X<T> to T*.

I still don't quite understand. If you want conversion from X<T> to T*,
just define operator T*().

-Kevin
 
J

Joe Seigh

Kevin said:
Why would you need that? I suppose you could do something like this:

T* X<T>::eek:perator->()
{
return T<T>()->something;
}

But I can't think of any reason to need a temporary.

It's for a smart pointer. Temps are local. If I get a local temp
copy of the smart pointer generated, I can guarantee that the reference
count will not go to zero for the duration of the expression. I can't
make that guarantee if T* is generated directly.
I still don't quite understand. If you want conversion from X<T> to T*,
just define operator T*().

That didn't appear to work. I had a ctor Y<T>::Y(X<T>) and a
Y<T>::eek:perator T*() defined.

Joe Seigh
 
J

Joe Seigh

It's for a smart pointer. Temps are local. If I get a local temp
copy of the smart pointer generated, I can guarantee that the reference
count will not go to zero for the duration of the expression. I can't
make that guarantee if T* is generated directly.

Though I suppose I could just not define -> for X:: and force using a Y ctor
explicitly.

struct thing {
int z;
};

X<thing> p;

p->z; // error, not allowed
(Y<thing>(p))->z; // instead


I did sort of get it working but when I did the * operator, I got
*p doesn't work but **p does, so I'm not sure I know what is going
on anymore. To be consistent, p->z would have to not work but
p->->z would.

Joe Seigh
 
K

Kevin Goodsell

Joe said:
It's for a smart pointer. Temps are local. If I get a local temp
copy of the smart pointer generated, I can guarantee that the reference
count will not go to zero for the duration of the expression. I can't
make that guarantee if T* is generated directly.

That makes it a bit more clear, but still not totally clear. Do you
think you could post a very simple (but complete, so we can
copy-paste-compile) program that demonstrates this problem?

-Kevin
 
D

David B. Held

Joe Seigh said:
[...]
T * X<T>::eek:perator ->();

gives one level of conversion. I don't want that. Something
like

Are you sure you understand how smart pointers work?
Typically, operator->() just returns the underlying pointer,
and the compiler dereferences that pointer to get at the
actual member. Given the operator above, there should
be no problem with your later syntax:

struct thing {
int z;
};

X<thing> p;

p->z; // error, not allowed

The above should be fine.

(Y<thing>(p))->z; // instead

Not sure where this comes from.
[...]
I need a Y<T> temp generated.

Why? Does the temp c'tor or d'tor have side effects? That
would be very peculiar indeed.
Strictly speaking, I would think no overloading of -> should
be needed.
[...]

If you are writing a smart pointer, overloading operator->
is one of the most important things you need to do. But
why write YASP (Yet Another Smart Pointer)? Do take
a look at Loki::SmartPtr before reinventing the wheel for
the Nth time.

Dave
 
J

Joe Seigh

If you are writing a smart pointer, overloading operator->
is one of the most important things you need to do. But
why write YASP (Yet Another Smart Pointer)? Do take
a look at Loki::SmartPtr before reinventing the wheel for
the Nth time.

No, this is an atomic thread-safe smart pointer. You're
guaranteed to point to a valid object or null no matter
what (the same guarantee that Java pointers have). No
other C++ smart pointer can make that claim. They require
you to own or have a lock on a smart pointer to access it
or to dereference it.

But this whole thing of C++ treating -> differently from
* has me spooked and I have no explanation of what is
going on. When C++ evaluate an expression, that expression
is replaced by a value of the expression return type. Except
when that expression is a -> expression. Then just the left
hand part of the expression is replaced, the -> is left in,
and the expression is re-evaluated.

Joe Seigh
 
D

David B. Held

Joe Seigh said:
[...]
No, this is an atomic thread-safe smart pointer. You're
guaranteed to point to a valid object or null no matter
what (the same guarantee that Java pointers have). No
other C++ smart pointer can make that claim.
LOL!!!

They require you to own or have a lock on a smart
pointer to access it or to dereference it.

You really do need to read Modern C++ Design, and pay
close attention to the chapter on smart pointers.
But this whole thing of C++ treating -> differently from
* has me spooked and I have no explanation of what is
going on.

Simple. operator->() for smart pointers should always
return an underlying pointer. operator*() should return
a reference to the pointed-to object.
When C++ evaluate an expression, that expression
is replaced by a value of the expression return type.
Except when that expression is a -> expression. Then
just the left hand part of the expression is replaced, the
-> is left in, and the expression is re-evaluated.

That's so that it's easy to write operator->(). Imagine if
you had to compute struct member offsets yourself.

Dave
 
J

Joe Seigh

David B. Held said:
Joe Seigh said:
[...]
No, this is an atomic thread-safe smart pointer. You're
guaranteed to point to a valid object or null no matter
what (the same guarantee that Java pointers have). No
other C++ smart pointer can make that claim.

LOL!!!

Atomic means that for the expression "p->a" where p is a
smart pointer, a valid value (or null pointer exception)
will be returned even if some other thread deletes or
modifies p during the evaluation of that expression. What
will not happen is a value returned from storage that
has been reallocated as another object in the meantime.
AFAIK all other smart pointers restrict what can happen
to p during the evaluation of such an expression.

The trick here (among other things) is to generate a local
copy of the pointer as an expression temp during the
evaluation of the expression. Since temps aren't dtored
until after the the evaluation of the expression, the
validity of the raw pointer value is guaranteed.

The reason you don't see any of the other smart pointers
using this trick is efficient thread-safe copying of the
smart pointer is non-trivial.

Joe Seigh
 
J

Joe Seigh

Rob said:
We really need to know what you are trying to do, otherwise
all you can expect is guess, If you havent got any code to show
perhapse give an example of the code whant to write and what you
want it to actually do.

Here's some code that illustrates the problem. It is not a smart pointer
implementation. Just an illustration of a particular operator overloading
issue.

template<typename T> class X; // forward declare

template<typename T> class Y {
T * ptr;
public:
Y(X<T> & z) {
ptr = z.ptr;
}
T * operator ->() { return ptr; }
T & operator *() { return *ptr; }
};

template<typename T> class X {
friend class Y<T>;
T * ptr;
public:
X(T * p = 0) : ptr(p) {}
Y<T> operator ->() { return Y<T>(*this); }
Y<T> operator *() { return Y<T>(*this); }
};

class Item {
public:
Item(int x = 0) : z(x) {}
int z;
};

int main(int argc, char *argv[]) {
X<Item> p = new Item(99);
Item k;
int n;

n = p->z;

k = **p; // why 2 *'s and not 2 ->'s above?

n = Y<Item>(p)->z; // this works

n = (Y<Item>(p))->z; // this doesn't, vc++6.0 complains

return 0;
}

Joe Seigh
 
R

Rob Williscroft

Joe Seigh wrote in
Rob Williscroft wrote:

[snip]


Here's some code that illustrates the problem. It is not a smart
pointer implementation. Just an illustration of a particular operator
overloading issue.

[snip]


int main(int argc, char *argv[]) {
X<Item> p = new Item(99);
Item k;
int n;

n = p->z;

k = **p; // why 2 *'s and not 2 ->'s above?

Because thats the way operator *() works, You can argue that its
wrong (or not the most versitile way it could work) but changing
it would break code.
n = Y<Item>(p)->z; // this works

n = (Y<Item>(p))->z; // this doesn't, vc++6.0 complains

Get a better compiler VC 6.0 is quiet old now, VC 7.1 handles it fine
BTW. Also this is clearly a bug so maybe its fixed by a service pack.
return 0;
}

Unfortunatly we all have to work with the language as its defined,
"warts and all", Its even worse for VC 6.0 users as it's a pre-standard
compiler.

You're example illustrated what you can't do with the language.

Perhapse an example of what you wan't to achive would help here,
i.e. What insterface you want to expose and some examples of the
usage you expect.

Rob.
 
D

David B. Held

Joe Seigh said:
David B. Held said:
[...]
LOL!!!

Atomic means that for the expression "p->a" where p
is a smart pointer, a valid value (or null pointer exception)
will be returned even if some other thread deletes or
modifies p during the evaluation of that expression.

I wasn't laughing at the idea of a thread-safe pointer.
I was laughing at the idea that you know every other
type of smart pointer in existence. There is no doubt
in my mind that there are hundreds of smart pointer
types that you have never seen because they have not
been released to the public. So to say that: "No
other C++ smart pointer can make that claim" is just
ridiculous.
What will not happen is a value returned from storage
that has been reallocated as another object in the
meantime.

I can see why you want to do that, but ask yourself if
this is the right level of locking. After all, most of the
time that you want to access a resource in multiple
threads, you also want to lock it for longer than the
duration of one pointer access. So, for instance, if
you do two successive pointer accesses into the same
struct, it seems to me that you are locking the pointee
twice, instead of once. So what are you paying for
this "convenience"?
AFAIK all other smart pointers restrict what can
happen to p during the evaluation of such an
expression.

And how many smart pointers do you know about?
The other question is, if other smart pointers typically
don't provide the functionality you wish to add, could
there be a good reason for doing not so?
The trick here (among other things) is to generate a
local copy of the pointer as an expression temp during
the evaluation of the expression. Since temps aren't
dtored until after the the evaluation of the expression,
the validity of the raw pointer value is guaranteed.

I don't see how that prevents another pointer to the
same resource from deleting the resource out from
under you. After all, the standard is thread-agnostic,
so it makes no guarantees about program behaviour
in the presence of multiple threads. In particular, I
don't think it follows at all that a temp has some magical
property which influences other threads. But I could
just be misunderstanding your explanation.
The reason you don't see any of the other smart
pointers using this trick is efficient thread-safe copying
of the smart pointer is non-trivial.

I think there's more reasons than that.

Dave
 
A

Alexander Terekhov

:
[...]
The other question is, if other smart pointers typically
don't provide the functionality you wish to add, could
there be a good reason for doing not so?

His pointer provides STRONG thread-safety, not {more common} BASIC one.
It's needed to sort of emulate {revised} Java volatile references with
automatic garbage collection for things like:

http://www.hpl.hp.com/personal/Hans_Boehm/gc/example.html

or similar stuff using DCAS/whatever (avoiding blocking; "lock-free").

regards,
alexander.
 
J

Joe Seigh

David B. Held said:
Joe Seigh said:
David B. Held said:
[...]
LOL!!!

Atomic means that for the expression "p->a" where p
is a smart pointer, a valid value (or null pointer exception)
will be returned even if some other thread deletes or
modifies p during the evaluation of that expression.

I wasn't laughing at the idea of a thread-safe pointer.
I was laughing at the idea that you know every other
type of smart pointer in existence. There is no doubt
in my mind that there are hundreds of smart pointer
types that you have never seen because they have not
been released to the public. So to say that: "No
other C++ smart pointer can make that claim" is just
ridiculous.

Well, there's "Lock-Free Reference Counting" by Detlifs et al,
but I don't know anyone using that since it requires something
like the MC68020 with a DCAS instruction. There's weighted
reference counting and something equivalent to it but my
impression is that they weren't efficient enough for practical
use. Reference counting is a form of GC, but you could use
another form of GC such as RCU or Maged Michael's hazard
pointers to make the refcount increment safe. Interestingly
enough if you use Michael's double check logic in conjuction
with LL/SC or ldwarx/stwcx instructions you can also increment
the refcount safely.
I can see why you want to do that, but ask yourself if
this is the right level of locking. After all, most of the
time that you want to access a resource in multiple
threads, you also want to lock it for longer than the
duration of one pointer access. So, for instance, if
you do two successive pointer accesses into the same
struct, it seems to me that you are locking the pointee
twice, instead of once. So what are you paying for
this "convenience"?

There's two classes of this pointer. A global shared pointer
class and a local non shared pointer class. The global
is for mainly for use in the actual data structure and the
local is for threads to access the struction. The local
pointers have overhead equal to the non-atomic threadsafe
pointers.

It's not so much that lock-free pointers are all that fast
by themselves, but they do let you implement other lock-free
solutions that do blow conventional locking solutions out
of the water.
And how many smart pointers do you know about?
The other question is, if other smart pointers typically
don't provide the functionality you wish to add, could
there be a good reason for doing not so?

Boost shared_ptr mainly. Either they didn't know how
to without adding more overhead than they wanted or they
decided it wasn't needed since they were requiring a
higher level of locking anyway. The latter is a
perfectly valid reason and seems to be the official
reason given.
I don't see how that prevents another pointer to the
same resource from deleting the resource out from
under you. After all, the standard is thread-agnostic,
so it makes no guarantees about program behaviour
in the presence of multiple threads. In particular, I
don't think it follows at all that a temp has some magical
property which influences other threads. But I could
just be misunderstanding your explanation.

The temp local copy of the pointer prevents the reference
count from going to zero during the evaluation of the
expression. If some other thread had deleted the global
pointer during the evaluation of the expression then the
dtor of the temp copy would actually delete the object
after the expression was evaluated, not during or before.

But I'm not trying sell anyone on this particular smart pointer.
The OP is about some compiler or language behavior I'm seeing
when trying to force the temp copy to be generated. It could
be this whole double conversion technique is not really supported
in C++ and I'd have do it in C instead.

Joe Seigh
 
T

tom_usenet

Is there a good write on this. The textbooks I have fluff over
on this? Specifically, I trying to dereference with 2 levels
of type conversion not 1, i.e.

X<T> -> z => Y<T> -> z => T* -> z
and
*X<T> => *Y<T> => *T


The Y<T> conversion has to be done as an expression temp. It cannot be done
inside a method body for X.

I think I sort of had it working for the -> operator but the same
technique doesn't work for the * operator. The * appears to getting
consumed.

operator-> is a bit odd. It's not that operator* gets consumed, but
that operator-> is recursive.

I think you need smart references, which don't exist yet. e.g.

smart_reference<T> operator*();

where the reference behaves almost exactly as if it were a T&, but you
can do stuff in the destructor (such as unlock a mutex or decrement a
reference count). Since you can't overload "operator." this isn't
currently possible.

Even without operator., you can still write an acceptable version of
smart_reference by providing operator T&, operator=, etc. You just
won't be able to do:

(*p).doit();

You could easily argue that users should write:

p->doit();

anyway, so you might not consider it a problem.

Tom
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,755
Messages
2,569,536
Members
45,012
Latest member
RoxanneDzm

Latest Threads

Top