A subtle access issue (may be advanced :-) )

A

Alf P. Steinbach

The following code compiles as-is with g++ and Comeau Online, but not when then
the commented lines are uncommented:


<code>
#include <stddef.h>

template< class T > T* create();

class Base
{
template< class T > friend T* create();
private:
static void* operator new( size_t size )
{
return ::eek:perator new( size );
}

// static void operator delete( void* p )
// {
// ::eek:perator delete( p );
// }

protected:
virtual ~Base() {}
};

class Derived
: public Base
{
public:
Derived() {}
virtual ~Derived() {}
};

template< class T >
T* create() { return new T; }

int main()
{
create<Derived>();
}
</code>


With uncommenting the commented code both compilers complain that the Derived
destructor can't access Base::eek:perator delete.

They don't complain that the Derived constructor can't access Base::eek:perator new.

I can understand the lack of complaint for the constructor: the constructor
doesn't need to access the allocation function, which is invoked by the new
expression which is in the context of the create function which has access.

I don't understand the complaint for the destructor. The new expression has to
potentially deallocate, if an expression is thrown. But it manages well to use
the allocation function, so why can't it also use the deallocation function?

In short, why this different treatment?

It almost seems as if the standard supports an implementation technique where
the call to the deallocation function is made directly from *within* the most
derived class' destructor?


Cheers & TIA.,

- Alf
 
J

James Kanze

The following code compiles as-is with g++ and Comeau Online,
but not when then the commented lines are uncommented:
<code>
#include <stddef.h>
template< class T > T* create();
class Base
{
template< class T > friend T* create();
private:
static void* operator new( size_t size )
{
return ::eek:perator new( size );
}
// static void operator delete( void* p )
// {
// ::eek:perator delete( p );
// }
protected:
virtual ~Base() {}

};
class Derived
: public Base
{
public:
Derived() {}
virtual ~Derived() {}
};
template< class T >
T* create() { return new T; }
int main()
{
create<Derived>();
}
</code>
With uncommenting the commented code both compilers complain
that the Derived destructor can't access Base::eek:perator
delete.

Correctly (more or less). There are two potential problems, the
first I'm certain of, the second less so:

The first is simple: anytime you have a new expression, the
compiler must be able to access operator delete at the point of
the new expression, since if the constructor exits by an
exception, the corresponding delete will be called. (I.e. if it
is a placement new, placement delete will be called.)

The second is a bit more subtle, and I'm not too sure what the
standard says (and I'm not sure where it says it, and am too
lazy to spend a quarter of an hour or more looking for it in all
of the possible places), but one frequent implementation of
destructors implies passing a hidden argument stating whether
operator delete should be called or not from the destructor.
The reason for this is that the correct operator delete is the
one which is correct for the most derived class, and the
compiler can't know which one this is in the delete expression;
some derived class may have provided its class specific delete.
I seem to recall, vaguely, that the standard endorses this
solution by requiring access control on operator delete in the
destructor---but as I said, I'm not sure, and I have even less
of an idea where to look in the standard to find it.
They don't complain that the Derived constructor can't access
Base::eek:perator new.

Because the Derived constructor doesn't need to access operator
new. At the construction site, the final class is known to the
compiler.
I can understand the lack of complaint for the constructor:
the constructor doesn't need to access the allocation
function, which is invoked by the new expression which is in
the context of the create function which has access.
I don't understand the complaint for the destructor. The new
expression has to potentially deallocate, if an expression is
thrown. But it manages well to use the allocation function, so
why can't it also use the deallocation function?
In short, why this different treatment?

In short: history and implementation techniques.
It almost seems as if the standard supports an implementation
technique where the call to the deallocation function is made
directly from *within* the most derived class' destructor?

More than "seems". Historically, at least, and maybe still,
that's by far the most frequent implementation technique.

The reason is simple: when I write "new Something", the compiler
knows the most derived class (Something). When I write "delete
pointer", however, the compiler doesn't know, so it has to call
the (virtual) destructor to find out. Other implementation
techniques are possible, but I can't really figure out one which
would correctly implement the access controls, e.g. reject the
delete (at compile time) if the dynamic type of the pointed to
object had a private operator delete.
 
A

Alf P. Steinbach

* James Kanze:
Correctly (more or less).

As you write further down, you don't *know* that.

And neither do I. :)

But one might suspect that the destructor needing access is correct wrt. to the
standard, and that the standard therefore ideally should be corrected, for
access has nothing to do with machine code.

There are two potential problems, the
first I'm certain of, the second less so:

The first is simple: anytime you have a new expression, the
compiler must be able to access operator delete at the point of
the new expression, since if the constructor exits by an
exception, the corresponding delete will be called. (I.e. if it
is a placement new, placement delete will be called.)

Yes, since I explained that further down in the (very short) original article, I
know that.

The second is a bit more subtle, and I'm not too sure what the
standard says (and I'm not sure where it says it, and am too
lazy to spend a quarter of an hour or more looking for it in all
of the possible places), but one frequent implementation of
destructors implies passing a hidden argument stating whether
operator delete should be called or not from the destructor.
The reason for this is that the correct operator delete is the
one which is correct for the most derived class, and the
compiler can't know which one this is in the delete expression;
some derived class may have provided its class specific delete.
I seem to recall, vaguely, that the standard endorses this
solution by requiring access control on operator delete in the
destructor---but as I said, I'm not sure, and I have even less
of an idea where to look in the standard to find it.

Yes, I explained also this further down in the (very short) original article.

The issue is, however, why the heck *access* should be affected.

Given that the code compiles fine with at least one common compiler it's not a
problem for the compiler to generate machine code to do what the (uncommented)
code expresses, and then the access rules should ideally be unaffected by the
implementation detail of how exactly things are done at the machine code level.

For access isn't about machine code.

It's about logical access.


Cheers & thanks (but didn't help, and I think standard's probably screwed up),

- Alf
 
L

ld

The following code compiles as-is with g++ and Comeau Online, but not when then
the commented lines are uncommented:

<code>
#include <stddef.h>

template< class T > T* create();

class Base
{
template< class T > friend T* create();
private:
     static void* operator new( size_t size )
     {
         return ::eek:perator new( size );
     }

//     static void operator delete( void* p )
//     {
//         ::eek:perator delete( p );
//     }

protected:
     virtual ~Base() {}

};

class Derived
     : public Base
{
public:
     Derived() {}
     virtual ~Derived() {}

};

template< class T >
T* create() { return new T; }

int main()
{
     create<Derived>();}

</code>

With uncommenting the commented code both compilers complain that the Derived
destructor can't access Base::eek:perator delete.

They don't complain that the Derived constructor can't access Base::eek:perator new.

I can understand the lack of complaint for the constructor: the constructor
doesn't need to access the allocation function, which is invoked by the new
expression which is in the context of the create function which has access.

I don't understand the complaint for the destructor. The new expression has to
potentially deallocate, if an expression is thrown. But it manages well to use
the allocation function, so why can't it also use the deallocation function?

In short, why this different treatment?

It almost seems as if the standard supports an implementation technique where
the call to the deallocation function is made directly from *within* the most
derived class' destructor?

yes, this is exactly for this reason. From the standard, the only
reference I can see is the first sentence in the section of delete (c+
+98 §5.3.5) "The delete-expression operator destroys a most derived
object or array created by a new-expression". There is no motivation
for this statement in the standard, but you can find the explanation
in TCPL3 (SE) in §15.6 (Free Store).

The operator delete must know the size of the object to destroy. Since
the delete operator is always static and it's first argument is of
type void*, the two solutions to know the object's size are either the
operator delete is called from the destructor of the most derived
class where the exact object's size is known and passed (e.g.
Base::eek:perator delete(p, sizeof(Derived))), either the size is stored
with the object (or elsewhere) and the delete operator knows how to
retrieve it (e.g. malloc-like implementation). In fact these two
approach are equivalent and depend on the implementation which is
unspecified by the standard. This explains why some implementation
require the access to the operator delete of Base from the destructor
of Derived (i.e. called within ~Derived()) and some don't (called
outside ~Derived()). Since the former is more efficient in both space
and speed, it tends to be the common behavior of decent compilers.

BTW, why don't you declare Base::eek:perator delete as protected instead
of private since its role is more or less the same as the destructor
~Base() ?

Cheers,

ld.
 
A

Alf P. Steinbach

* ld:
[snip explanation of machine code level -- I don't think it should influence
access!]

BTW, why don't you declare Base::eek:perator delete as protected instead
of private since its role is more or less the same as the destructor
~Base() ?

That's what I had to do, but the reason for the "private:" is to express in the
language that a derived class' code can't do 'delete this'.

With protected access it can. :-(

Cheers,

ld.

Thanks :) Although I disagree (very strongly) that access rules should be
affected by possible implementation techniques for the compiler.


Cheers,

- Alf
 
G

Gil

I don't understand the complaint for the destructor.

At the point of definition of a virtual destructor, non-placement
operator delete shall be looked up in the scope of the destructor’s
class
and if found shall be accessible and unambiguous.
 
A

Alf P. Steinbach

* Gil:
At the point of definition of a virtual destructor, non-placement
operator delete shall be looked up in the scope of the destructor’s
class
and if found shall be accessible and unambiguous.

Where does it say that?


Cheers,

- Alf
 
L

ld

* ld:





[snip explanation of machine code level -- I don't think it should influence
access!]
BTW, why don't you declare Base::eek:perator delete as protected instead
of private since its role is more or less the same as the destructor
~Base() ?

That's what I had to do, but the reason for the "private:" is to express in the
language that a derived class' code can't do 'delete this'.

With protected access it can. :-(



Thanks :) Although I disagree (very strongly) that access rules should be
affected by possible implementation techniques for the compiler.

I fully agree that it should not influence the access. I was just
giving a possible explanation why this pre-standard behavior may
happened. In particular it does not follow ISO/IEC 14882:1998(E) (Free
Store) 12.5 §8

"Access to the deallocation function is checked statically. Hence,
even though a different one might actually be executed, the statically
visible deallocation function is required to be accessible." [example
follow].

Therefore, the compiler should clearly report a diagnostic, whatever
implementation it uses.

Cheers,

ld.
 
A

Alf P. Steinbach

* Gil:
ISO/IEC 14882:2003(E) 12.4.11, sorry it's late.

Thank you.

I think this "be accessible" wording in §12.4/11, together with corresponding
wording in §12.5/4 about the accessibility of the *dynamically* chosen
deallocation function, constitutes a double defect in the standard.

Defect (A), the most obvious, is that §12.5/4 as I see it contains a
self-contradiction, while defect (B) goes to the rationale.

For defect (A), self-contradiction, §12.5/4 says that "if the delete-expression
is used to deallocate a class whose static type has a virtual destructor, the
deallocation function is the one found by the lookup in the definition of the
dynamic type's virtual destructor. ... If the result of the lookup is ambiguous
or inaccessible ... the program is ill-formed".

If the word "inaccessible" here talks about whether the delete expression itself
has access to the dynamically chosen deallocation function, then any program
doing 'delete p' where p is of static type Base* and the dynamic type of *p is
Derived, where Derived has a protected or private deallocation function, and the
delete expression isn't in a context with access to such things in Derived,
would be ill-formed. Which is clearly not the case. So that interpretation is
not tenable.

On the other hand, if the word "inaccessible" here talks about whether the
dynamic type's destructor has access to the dynamically chosen deallocation
function, then a delete expression would be valid as long as that's the case,
even if the delete expression itself does not have access (for nothing is then
said about that) to the deallocation function of the static type. Which clearly
isn't the case either. So that interpretation is not tenable.

Since neither possible choice of which function it is that needs access yields a
tenable interpretation, perhaps "the lookup" isn't meant to refer to the lookup
described earlier in the paragraph, but rather to a lookup in the static type of *p.


For defect (B), about the rationale of §12.4/11 (destructor needs access):

(1) nowhere else is static accessibility restricted by what function might be
dynamically selected for invocation,

(2) the restriction apparently serves no useful purpose except as an unnatural
and easily circumvented means of creating a "final" class, and

(3) the restriction disallows at least one useful expression of a restriction on
derived classes, namely restricting use of 'delete this' as in the code I
presented at the start of the thread.

Regarding (1), that this rule is contrary to the spirit of C++, consider the
inconsistency when compared to the situation for destructors:

class Base { virtual ~Base() {} };

class Derived
{
private:
virtual ~Derived() {}
public:
static Derived* newInstance() { return new Derived(); }
};

int main()
{
Base* p = Derived::newInstance();
delete p; // OK, even though no access to dynamically selected destructor.
}

Why should the delete not be OK just because the dynamically selected destructor
is inaccessible to the delete expression? That would be nonsense. What matters
wrt. accessibility, here and elsewhere in C++, is the static typing.

And so it's IMO also nonsense to have such a requirement for the dynamically
selected deallocation function. Especially considering that what it requires is
not even direct accessibility, but indirect accessibility: that the dynamically
selected deallocation function must be accessible to the dynamically selected
destructor. Of course that indirection is the only way to express something so
absurd as the dynamic choice having to be statically "accessible" in a sense.

Regarding (2), no apparent useful purpose, at least not any very evident one,
what C++ constructs would be permitted if this rule were replaced with plain
static accessibility (as one has everywhere else in C++)?

One could then write such dangerous code (note: irony) as

class Base1 { virtual ~Base1() {} };

class Base2
{
private:
void* operator new( size_t size ) { ... }
void operator delete( void* p ) { ... }
public:
virtual ~Base2() {}
static Base1* newBase1();
};

class Derived: public Base1, public Base2
{}; // AIUI not allowed, has virtual destructor but inaccessible op delete.

Base1* Base2::newBase1() { return new Derived; }

int main()
{
Base1* p = Base2::newBase1();
delete p;
}

The "trouble" here being that the delete dynamically invokes a statically
inaccessible deallocation function, just as -- but of course allowed in
current C++, since it's the normal rule! -- in the first example the delete
dynamically invokes a statically inaccessible destructor function.

Perhaps someone can come up with some purpose that isn't of the sort "be
inconsistent with the rest of C++", but currently I fail to see it.

Regarding (3), that the language's restriction disallows useful expressions of
programmer-selected restrictions on derived classes, the original code in this
thread is an example.


Cheers,

- Alf
 
A

Alf P. Steinbach

* ld:
* ld:




The following code compiles as-is with g++ and Comeau Online, but not when then
the commented lines are uncommented:
<code>
#include <stddef.h>
template< class T > T* create();
class Base
{
template< class T > friend T* create();
private:
static void* operator new( size_t size )
{
return ::eek:perator new( size );
}
// static void operator delete( void* p )
// {
// ::eek:perator delete( p );
// }
protected:
virtual ~Base() {}
};
class Derived
: public Base
{
public:
Derived() {}
virtual ~Derived() {}
};
template< class T >
T* create() { return new T; }
int main()
{
create<Derived>();}
</code>
With uncommenting the commented code both compilers complain that the Derived
destructor can't access Base::eek:perator delete.
They don't complain that the Derived constructor can't access Base::eek:perator new.
I can understand the lack of complaint for the constructor: the constructor
doesn't need to access the allocation function, which is invoked by the new
expression which is in the context of the create function which has access.
I don't understand the complaint for the destructor. The new expression has to
potentially deallocate, if an expression is thrown. But it manages well to use
the allocation function, so why can't it also use the deallocation function?
In short, why this different treatment?
It almost seems as if the standard supports an implementation technique where
the call to the deallocation function is made directly from *within* the most
derived class' destructor?
[snip explanation of machine code level -- I don't think it should influence
access!]
BTW, why don't you declare Base::eek:perator delete as protected instead
of private since its role is more or less the same as the destructor
~Base() ?
That's what I had to do, but the reason for the "private:" is to express in the
language that a derived class' code can't do 'delete this'.

With protected access it can. :-(


Cheers,
ld.
Thanks :) Although I disagree (very strongly) that access rules should be
affected by possible implementation techniques for the compiler.

I fully agree that it should not influence the access. I was just
giving a possible explanation why this pre-standard behavior may
happened. In particular it does not follow ISO/IEC 14882:1998(E) (Free
Store) 12.5 §8

"Access to the deallocation function is checked statically. Hence,
even though a different one might actually be executed, the statically
visible deallocation function is required to be accessible." [example
follow].

Therefore, the compiler should clearly report a diagnostic, whatever
implementation it uses.

Cheers,

ld.

Thanks, but in the given code the new expression does have static access, since
it's in a friend function. It's just the dynamically chosen destructor that
doesn't have access. And I think that rule of the standard is not as it should
be, possibly not even reflecting original intent.

But another thanks: I just posted a response else-thread where I pointed out
what I thought was a double defect in the standard.

Your observation (I didn't see that /8, which *retroactively* modifies the
meaning of /4!) means that I was wrong about one of them, the A in that article,
but I maintain the B of that article -- it's just very wrong to have static
access restricted by a possible dynamic choice, it's not that way anywhere else.


Cheers,

- Alf
 
J

James Kanze

* James Kanze:
As you write further down, you don't *know* that.

I do with regards to the new expression. The operator delete
must be accessible in the context of the new expression.
And neither do I. :)
But one might suspect that the destructor needing access is
correct wrt. to the standard, and that the standard therefore
ideally should be corrected, for access has nothing to do with
machine code.

I agree in principle; if that's what the standard says (and I
seem to recall that it is), then it's pretty ugly. On the other
hand, the authors of the standard do consider implementation
issues, and generally dislike requiring something that can't
reasonably be implemented.
 
A

Alf P. Steinbach

* James Kanze:
I do with regards to the new expression. The operator delete
must be accessible in the context of the new expression.

The operator delete is accessible in the context of the new expression, since
that expression appears in a friend function.

I agree in principle; if that's what the standard says (and I
seem to recall that it is), then it's pretty ugly. On the other
hand, the authors of the standard do consider implementation
issues, and generally dislike requiring something that can't
reasonably be implemented.

MSVC has no problem compiling the code with commented block uncommented.

It's very easy to implement.


Cheers,

- Alf
 

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

Forum statistics

Threads
473,795
Messages
2,569,644
Members
45,358
Latest member
TreyTritt8

Latest Threads

Top