I've been trying to find out a bit about what does and what does not break binary compatibility of a C++ library and was coming across this (http://developer.kde.org/documentation/other/binarycompatibility.html) site.
Most of it makes sense, but one point puzzles me:
You can...
change an inline function or make an inline function non-inline if it is safe that programs linked with the prior version of the library call the old implementation. This is tricky and might be dangerous. Think twice before doing it. Because of this, classes that are supposed to stay binary compatible should always have non-inline destructor, even if it's empty, otherwise the compiler will automatically generate an empty inlined one.
Now I understand that I cannot change an inline destructor without getting into danger, but when would I actually ever do that?
As I cannot add member variables (or rearrange member variables or make member variables of different types) anyways without breaking compatibilty, my destructor would actually have to contain some code to change in order for this rule to make sense. Hardly any of my destructors do, and if they do, it's something really trivial (not likely to change).
Am I overseeing something?
SuperKoko
March 5th, 2006, 09:06 AM
As I cannot add member variables (or rearrange member variables or make member variables of different types) anyways without breaking compatibilty, my destructor would actually have to contain some code to change in order for this rule to make sense. Hardly any of my destructors do, and if they do, it's something really trivial (not likely to change).
Someone may know that there will be extensions and "reserve" one or more fields.
class ExtensibleClass
{
int UsedMember;
float AnotherUsedMember;
void* ReservedForAnExtensionIamPlanning;
void* ReservedForGeneralExtension;
public:
// ...
ExtensibleClass();
~ExtensibleClass();
};
Thus, without changing the class size, and the binary compatibility, I can add data, dynamically allocated, in future releases.
I just need at least one void* reserved field.
If you want a maximal extensibility, ideally, your class should contain only a pointer to the actual implementation:
class MyClassImplementation; // forward declaration -- the true declaration is not available to the user
class MyClass
{
private:
MyClassImplementation* pInternalStructure;
public:
MyClass();
~MyClass();
// all the methods here.
};
But, it may cost speed (a level of indirection).
A more conventional way to do it (if the library is meant to be usable from the C language), is to declare a handle type as a pointer to a dummy structure.
typedef struct Dummy {} *MyClassHandle;
MyClassHandle CreateMyClassInstance();
FreeMyClassInstance(MyClassHandle);
/* other functions working with a MyClassHandle handle. */
This article (http://www.research.att.com/~bs/stack_cat.pdf) of Bjarne Stroustrup may get your attention.
SuperKoko
March 5th, 2006, 09:38 AM
Hey, treuss, after having written post #2, I followed your link (perhaps I should have done that previously), I saw that these d-pointers were mentioned.
You should read it, as well as the hash-table trick if no d-pointer is available.
treuss
March 5th, 2006, 10:23 AM
SuperKoko,
thanks for the link to the article. Unfortunately, I think you misunderstood my post a bit. I was not asking, what I can do to achieve to ensure my classes stay binary compatible. I have been using the d-pointer concept in classes where I was certain that I would change the implementation at some point, but due to the performance backdraw I do not frequently use it.
My question concerned the bold part: Why should my classes always define a non-inline destructor?
If my class uses a d-pointer, the destructor is very likely to contain only "delete d;" and that is very unlikely to change. Destructors do not change, if the data members do not.
So I must be missing some case where:
- Classes remain binary compatible if the destructor is defined non-inline
- Classes do not remain binary compatible, if the destructor is defined inline
Unfortunately, I do not see that case.
SuperKoko
March 5th, 2006, 11:11 AM
The first idea, is that the d-pointer is not used at all (not even initialized to NULL) in the first version of the library, but is reserved for future usage.
And, there is also a problem, with an inline destructor invoking "delete d" :
delete cannot be called on an incomplete type.
There is at least two reasons why the ISO standard forbids that:
The complete type may have overloaded operator new/operator delete.
The destructor may be virtual ... in that case, the destructor invocation must not be a static call, but a virtual call, which in turn, needs absolutely the full declaration of the class, because the position of the vtable pointer and the offset of the function-pointer-to-destructor in that vtable, both highly depends on the full declaration.
So, restricting future extensions, not using overloaded operators, and not using virtual destructors, one may think that writing that:
// an incomplete declaration which does not show implementation details
// but is needed in order to make delete work.
class InternalRepresentation
{
public:
~InternalRepresentation();
};
Instead of the forward declaration:
class InternalRepresentation;
May solve the problem.
But the behaviour remains undefined.
I fear that delete may need the complete declaration of the object, and use its size (thus, adding new fields is a problem).
The destructor's function signature may also depend on this declaration.
Actually, an implementation may choose to use a different "free store" depending on the size of the object.
For example, objects whose size is <=8 may use a very fast allocator allocating constant-size objects.
And, of course, since the new-expression in the shared library code, uses a structure which may have a size>8, it will not work properly when the delete-expression is called.
There are many more potential problems, since it is undefined, anything may happen.
exterminator
March 6th, 2006, 04:07 AM
Someone may know that there will be extensions and "reserve" one or more fields.....
....Thus, without changing the class size, and the binary compatibility, I can add data, dynamically allocated, in future releases.
I just need at least one void* reserved field.
I don't think such scenarios would affect what the requirement of binary compatibility is. Reason? Suppose you have a reserved field - basically a pointer that you would want to be cleaned up in the destructor. Even if it is being used or not being used - you can work-around that solution by initializing it to NULL - and in the destructor the delete call could be placed there. Deleting a NULL is not a problem. If you have a void* for a reserved field - you can have that in the code without affecting the compatibility as follows (for a member function):
template <typename T>
void Func(const T& t, void*)
{
//...
}
The trick is not providing the name to the parameter. Tomorrow, say you got something to name here for the void* parameter, as you say the reserved field - one can easily add it without breaking the function call signature and hence the compatibility. This might look like a comment valid for source compatibility (for sure) and I have a feeling that it would work in achieving binary compatibility as well but I am not very sure.
As far as having a destructor inline, be it virtual or not, causing problems in binary compatibility is concerned - I think having a destructor inlined, in general, is not a very good idea - when the class doesn't just contain POD members but contains non-POD types with non-trivial destructor. It causes code bloat. Moreover, since inline is something that you just suggest the compiler to do so and the compiler is not obliged to do it - I guess that may be something that the author of that text is suggesting not to do.
The constructors and destructors may look empty but might be doing a lot of stuff - that may prevent them from being inlined and if however they are inlined that may cause code bloat.
I got the following quote from a CUJ article:
A good example is the static invocation chain of base class destructors triggered by the virtual resolution of a derived class destructor. All the destructor calls except for the initial resolution are resolved statically.
This is said in regard to virtual destructors. In this case, I think if the base class is instantiated - the destructor would not be inlined. And hence a need for non-inline d-tor. If the clients of this binary are doing anything like this - you can usually be certain if the d-tor would be inlined or not. Also, it is wise enough in making the trivial virtual destructors inlined as that helps in saving a function call in case of class hierarchies.
As for treuss' question - even I am not able to figure out what is exactly that the author of that article suggests against the usage of inlined virtual destructors in general even when no members are added. What are the cases when the destructor code might change with us explicitly changing it?
May be it has something to do with uncertainities what the compiler/linker may actually be doing - modify the code of a self generated method (like the constructor destructor) on its own upon re-builds and hence fail the inlined code of the client. By the way, having non-inlined ones is certainly not going to cause problems. Regards.
treuss
March 6th, 2006, 04:52 AM
I don't think such scenarios would affect what the requirement of binary compatibility is. Reason? Suppose you have a reserved field - basically a pointer that you would want to be cleaned up in the destructor. Even if it is being used or not being used - you can work-around that solution by initializing it to NULL - and in the destructor the delete call could be placed there. Deleting a NULL is not a problem. Deleting a NULL pointer is a problem (undefined), if the pointer is void*.
If you have a "delete x;" in a destructor, the class of x must be completely known (as SuperKoko pointed out). So it is not possible to hide the class of x (in order to improve binary compatibility) without also "hiding" (making non-inline) the delete call, thus the destructor.
BUT: The compiler takes already care of that, by not allowing delete to be called for incomplete types, no reason for the programmer to worry about it.
If you have a void* for a reserved field - you can have that in the code without affecting the compatibility as follows (for a member function):
template <typename T>
void Func(const T& t, void*)
{
//...
}
Not sure what to think about your example. Templates are probably the worst thing to binary compatibility. As they are defined in the header the code will be generated when the application is built. Thus any changes to the code will not be reflected to an application built with an old version.
Besides, adding non-virtual functions does not break compatibility, so I see even less need for the code you show.
As far as having a destructor inline, be it virtual or not, causing problems in binary compatibility is concerned - I think having a destructor inlined, in general, is not a very good idea - when the class doesn't just contain POD members but contains non-POD types with non-trivial destructor. It causes code bloat.
As a matter of fact, I achieved some great performance improvements by putting some constructors and destructors inline. That was after a profile run showed my something like 80% of the time being spent in creating these small objects. Granted, the objects contained only POD.
The constructors and destructors may look empty but might be doing a lot of stuff - that may prevent them from being inlined and if however they are inlined that may cause code bloat.Code bloat is something, that concerns me very, very little. I think anyone not writing for embedded devices does not have to pay attention to code bloat, today.
May be it has something to do with uncertainities what the compiler/linker may actually be doing - modify the code of a self generated method (like the constructor destructor) on its own upon re-builds and hence fail the inlined code of the client. By the way, having non-inlined ones is certainly not going to cause problems. Regards.
OK, the question whether compiler behavior (inlining for the current version of the library while it failed to inline for an old version) can break compatibility is interessting. But this is not concerned with destructors only.
exterminator
March 6th, 2006, 05:36 AM
Deleting a NULL pointer is a problem (undefined), if the pointer is void*.Is it? Really? I did not know that.. can you provide me with a source for that information?If you have a "delete x;" in a destructor, the class of x must be completely known (as SuperKoko pointed out). So it is not possible to hide the class of x (in order to improve binary compatibility) without also "hiding" (making non-inline) the delete call, thus the destructor.Right.. thinking again all over it makes sense. Thanks.Not sure what to think about your example. Templates are probably the worst thing to binary compatibility. As they are defined in the header the code will be generated when the application is built. Thus any changes to the code will not be reflected to an application built with an old version.Forget about it .. I did not really mean to write a template - just wanted to convey "any type T" in the function argument - not a template. And consider it virtual as well. This point is subject to the conditions mentioned in the article linked by you for re-implementing the virtual functions.OK, the question whether compiler behavior (inlining for the current version of the library while it failed to inline for an old version) can break compatibility is interessting. But this is not concerned with destructors only.I said this because the code in the destructor (and the constructors) are played around a lot by the compilers. They put them implicitly and hence I thought in that direction.
I have a question here - is it guranteed that an inlined function will be inlined again if no changes are made to the function and the binary re-built?
treuss
March 6th, 2006, 05:48 AM
Is it? Really? I did not know that.. can you provide me with a source for that information?
gcc prints:deletenull.cc:8: warning: deleting `void*' is undefined. OK, probably I said it wrong: delete on a null pointer is most likely defined, but if the pointer points to an object, the object cannot be deleted via the void* pointer.
I have a question here - is it guranteed that an inlined function will be inlined again if no changes are made to the function and the binary re-built?This is an issue of the compilers, so you can get the answer by either asking Microsoft or by reading the gcc sources - LOL!
SuperKoko
March 6th, 2006, 06:12 AM
I have a question here - is it guranteed that an inlined function will be inlined again if no changes are made to the function and the binary re-built?
No.
Whether a function is inlined or not depend on the invokation context.
For example, a good compiler should inline a big inline function, if there is only one call to this function in the translation unit, because there is no code duplication.
If a second function call is added, the compiler may choose to not inline it.
The compiler may also choose to inline or not inline a function, using an euristhic depending on the cost of the function call.
And, depending on the actual arguments of the inline function, one call may be adequate to a __cdecl function call, and another may be adequate to inlining.
For example:
struct TwoValues {int a; int b;};
inline void InlineFunction(int a, int b)
{
// some code here.
}
extern void SetTwoValue(TwoValues&);
int main()
{
// code here
{
TwoValues val;
SetTwoValue(&val);
InlineFunction(val.a,val.b);
}
// code here
}
The compiler may see that after the SetTwoValue function call, the stack as the exact state needed for a __cdecl or __stdcall function call to SetTwoValue.
And, since the val variable is destroyed, there is no problem to use the variable as arguments.
Thus, this function call is not very expensive.
On the other side if the two arguments a and b were constant values, the compiler would need a few assembly instructions to put them on the stack, thus inlining would be better.
Is it? Really? I did not know that.. can you provide me with a source for that information?
There is the GCC warning:
"deleting ‘void*’ is undefined"
Even if deleting a NULL pointer with void* may work, I am sure that deleting a pointer to non-POD object via a void* pointer is incorrect. It will do at least a memory leak (since the destructor is not invoked), but may also crash.
And, the standard says:
3 In the first alternative (delete object), if the static type of the
operand is different from its dynamic type, the static type shall be a
base class of the operand's dynamic type and the static type shall
have a virtual destructor or the behavior is undefined. In the second
alternative (delete array) if the dynamic type of the object to be
deleted differs from its static type, the behavior is undefined.19)
And, the footnote 19 is:
19) This implies that an object cannot be deleted using a pointer of
type void* because there are no objects of type void.
So, since void* has no virtual destructor, and that there is no object of type void (even POD types are in a sense "derived" from void), it is always undefined to delete a void* pointer.
Lepinok
March 6th, 2006, 07:30 AM
Also all of those examples about the compiling processes demonstrate how the cut-off mechanism works. Whether ot not to recompile the source code depends on whether not the signature has been changed. Inline I think isn't about code compiling but code generation instead...
Regards,
codeguru.com
Copyright Internet.com Inc., All Rights Reserved.