EBCO - why does it require inheritance?

C

Chris Fairles

I recently implemented c++0x variants of tuple to take advantage of
ECBO and then implemented unique_ptr using a tuple to hold the pointer
- deleter pair so that sizeof(unique_ptr<int>) == sizeof(int*).

It then came to my attention that its a lot of hoops to jump through
just to take advantage of ECBO. I'm wondering whats preventing
something like :

struct SomeEmptyType {void f(){}};

struct A {
SomeEmptyType t;
int * i;
};

from having a sizeof(int*) and not sizeof(int*) + 1 + padding to align
with next boundary?

I know the standard says that "Complete objects and member subobjects
of class type shall have nonzero size." and "Base class subobjects are
not so constrained." but why? Is there some C-compatibility issue?

I think "A a; reinterpret_cast<SomeEmptyType*>(&a)->f();" could still
be well formed even if t had no storage or is this not possible to do
for compiler implementors?
 
J

jg

just to take advantage of ECBO. I'm wondering whats preventing
something like :

struct SomeEmptyType {void f(){}};

struct A {
SomeEmptyType t;
int * i;

};

from having a sizeof(int*) and not sizeof(int*) + 1 + padding to align
with next boundary?

t is 1 byte and the total is 8 bytes (3 bytes padding). It cannot be
4 bytes.
I know the standard says that "Complete objects and member subobjects
of class type shall have nonzero size." and "Base class subobjects are
not so constrained." but why? Is there some C-compatibility issue?

Your example isn't about inheritance (class A : public B {} is). So
the above statement does not apply to your example.


JG
 
J

Joe Gottman

jg said:
t is 1 byte and the total is 8 bytes (3 bytes padding). It cannot be
4 bytes.


I think it has to do with subobjects having unique addresses. Let's
change your example to
struct A {
SomeEmptyType t1;
SomeEmptyType t2;
int *1;
}

Then if we create an object a of type A, we would have to have &a.t1
!= &a.t2, and similarly with member-pointers.

Joe Gottman
 
C

Chris Fairles

I think it has to do with subobjects having unique addresses. Let's
change your example to
struct A {
SomeEmptyType t1;
SomeEmptyType t2;
int *1;

}

Then if we create an object a of type A, we would have to have &a.t1
!= &a.t2, and similarly with member-pointers.

Joe Gottman

Ok, but if SomeEmptyType has no state, can you think of a use-case
that requires t1 and t2 to have unique addresses? Otherwise isn't
accessing t1 functionally equivolent to accessing t2 (and
substitutable in all cases?).

I can see if you were using the address of the object as its state,
then yes, they have to be unqiue but I can't think of a case where I'd
need that behavior.

Chris
 
J

Joe Greer

Chris Fairles said:
Ok, but if SomeEmptyType has no state, can you think of a use-case
that requires t1 and t2 to have unique addresses? Otherwise isn't
accessing t1 functionally equivolent to accessing t2 (and
substitutable in all cases?).

I can see if you were using the address of the object as its state,
then yes, they have to be unqiue but I can't think of a case where I'd
need that behavior.

One can turn that around a bit and ask why you need a stateless,
non-virtual class in the first place? In my book that's called
a namespace with free functions. No added overhead whatsoever.

But, as to your question, each object declared in a class/struct is
considered a separate instance of that class. Make sense so far? This
means that while the values may be the same, the object identities are not.
In C++, object identity is spelled &member. Therefore if &t1 == &t2, that
would mean that the two object instances were the same object and they are
not. They just have the same value. Make sense?

joe
 
J

Joe Greer

Ok, but if SomeEmptyType has no state, can you think of a use-case
that requires t1 and t2 to have unique addresses? Otherwise isn't
accessing t1 functionally equivolent to accessing t2 (and
substitutable in all cases?).

I can see if you were using the address of the object as its state,
then yes, they have to be unqiue but I can't think of a case where I'd
need that behavior.

Well, if SomeEmptyType is stateless and isn't some abstract base class,
then it is best spelled 'namespace' in my opinion. Then you aren't
creating an object for no purpose.

Creating objects is the crux of your problem. When you declare a t1 and
a t2, you are declaring to separate object instances. These instances
have the same value, but are not the same object. In normal object-
oriented terms, you would say that they didn't have the same object
identity. That is, if I declared two ints i and j. I assigned both of
them the value 1. We would then say that i == j, but we would not say
that i and j were the same int. Same thing here. We declared t1 and
t2. We might possibly say that t1 == t2 (because they were both empty)
but we would not say that t1 and t2 were the same object. Clear so far?
In C++ object identity is determined by its address. Therefore if t1
and t2 were to maintain separate identities &t1 != &t2. This is a
fundamental property of object oriented programming and not lightly
broken especially if there is so easy a way to get what you want without
breaking it.

So, we change the code to:

namespace SomeEmptyType
{
void f();
}

struct A {
int *i;
}

using namespace SomeEmptyType;

f();

-or-

class SomeEmptyType
{
public:
static void f();
};

struct A {
int *i;
}

SomeEmptyType::f();


and we are in the same boat as before. There is nothing to be gained in
accessing f() via an instance of A, so why tie them together?

joe
 
C

Chris Fairles

One can turn that around a bit and ask why you need a stateless,
non-virtual class in the first place? In my book that's called
a namespace with free functions. No added overhead whatsoever.

But, as to your question, each object declared in a class/struct is
considered a separate instance of that class. Make sense so far? This
means that while the values may be the same, the object identities are not.
In C++, object identity is spelled &member. Therefore if &t1 == &t2, that
would mean that the two object instances were the same object and they are
not. They just have the same value. Make sense?

joe
This all came about while implementing unique_ptr for c++0x.

template <class _Tp>
struct default_delete : public unary_function<_Tp*,void> {
default_delete() {}
template <class _Up> default_delete(const default_delete<_Up>&) {}
void operator()(_Tp* __ptr) const {
static_assert(sizeof(_Tp) > 0, "can't delete pointer to incomplete
type");
delete __ptr;
}
};

template <class _Tp, class _Tp_Deleter = default_delete<_Tp>>
class unique_ptr {
/* ... */

_Tp* __ptr;
_Tp_Deleter __deleter;
};

However, I wanted sizeof(unique_ptr<int>) == sizeof(int*) which
requries EBCO applied somehow. What I did was implement EBCO for
tuples and store the pair as tuple<_Tp*,_Tp_Deleter> but I started
wondering why isn't it possible for compilers to determine that
__deleter is actually empty, its address is never taken (perhaps
theres no realiable way to determine this, I'm not a compiler writer
heh), so don't allocate storage for it.

I just couldn't come up with a good argument (besides quoting the
standard) when someone looks at the required code to get ebco and
claims "C++ is stupid. Why do you have to jump through hoops and re-
design your class just to not waste 4/8 bytes required to store an
empty, stateless object. You need convoluted and non-obvious solutions
to a seemingly trivial problem."

Cheers,
Chris
 
J

Joe Greer

This all came about while implementing unique_ptr for c++0x.

template <class _Tp>
struct default_delete : public unary_function<_Tp*,void> {
default_delete() {}
template <class _Up> default_delete(const default_delete<_Up>&) {}
void operator()(_Tp* __ptr) const {
static_assert(sizeof(_Tp) > 0, "can't delete pointer to incomplete
type");
delete __ptr;
}
};

template <class _Tp, class _Tp_Deleter = default_delete<_Tp>>
class unique_ptr {
/* ... */

_Tp* __ptr;
_Tp_Deleter __deleter;
};

Given the nature of your deleter's, you can either make the method static like:

template <class _Tp>
struct default_delete : public unary_function<_Tp*,void> {
default_delete() {} template <class _Up> default_delete(const default_delete
_Up>&) {}
static void delete(_Tp* __ptr) const {
static_assert(sizeof(_Tp) > 0, "can't delete pointer to incomplete
type");
delete __ptr;
}
};

template <class _Tp, class _Tp_Deleter = default_delete<_Tp>>
class unique_ptr {
/* ... */

_Tp* __ptr;
~unique_ptr() { _Tp_Deleter::delete(__ptr); }
};

or you can instatiate it when you need it rather than at construction time.

template <class _Tp, class _Tp_Deleter = default_delete<_Tp>>
class unique_ptr {
/* ... */

_Tp* __ptr;
~unique_ptr() { _Tp_Deleter()(__ptr); }
};


Just some thoughts,
joe
 
C

Chris Fairles

Given the nature of your deleter's, you can either make the method static like:

template <class _Tp>
struct default_delete : public unary_function<_Tp*,void> {
default_delete() {} template <class _Up> default_delete(const default_delete
_Up>&) {}
static void delete(_Tp* __ptr) const {
static_assert(sizeof(_Tp) > 0, "can't delete pointer to incomplete
type");
delete __ptr;
}
};

template <class _Tp, class _Tp_Deleter = default_delete<_Tp>>
class unique_ptr {
/* ... */

_Tp* __ptr;
~unique_ptr() { _Tp_Deleter::delete(__ptr); }
};

A good idea, but I don't think this will work when considering user-
defined deleters. It would require deleters to have a static function
but the standard allows the deleter to have non-static state that can
be accessed within the member func (operator or not) that does the
delete.
or you can instatiate it when you need it rather than at construction time.

template <class _Tp, class _Tp_Deleter = default_delete<_Tp>>
class unique_ptr {
/* ... */

_Tp* __ptr;
~unique_ptr() { _Tp_Deleter()(__ptr); }
};

Again, due to deleters having state, and unique_ptr's being able to
store references to deleters, the following is well formed:

struct deleter
{
deleter(std::string msg):m(msg){}
void operator()(int *) {std::cout << msg; delete i;}
std::string m;
};
deleter d("hi mom");
std::unique_ptr<int,deleter&>(new int, d);
//or std::unique_ptr<int,deleter>(new int, deleter("hi mom")) etc.

So instantiation-on-use won't work so long as the above is allowed.
Just some thoughts,

Much appreciated.

Chris
 

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,755
Messages
2,569,537
Members
45,020
Latest member
GenesisGai

Latest Threads

Top