Ok, let me summarize the whole thing.
Your (correct!) answers say that in C++ binary compatibility * is never guaranteed for different types. It's undefined behavior to take the value of a memory area where a variable is located, and use it for a variable of a different type (and this most likely should be avoided also with variables of the same type).
Also in real-life this thing could be dangerous even for simple objects, never mind containers!
*: by binary compatibility I mean that the same values is stored in memory in the same way and that the same assembly instruction are used at the same way to manuipulate it. eg: even if float
and int
are 4 bytes each, they are not be binary compatible.
However I'm not satisfied by this C++ rule: let's focus on a single case, like on these two structures: struct A{ int a[1000000]; };
and struct B{ int a[1000000]; };
.
We can't just use the address of an A
object as if it was a B
one. And this frustrates me for the following reasons:
The compiler statically knows if those structures are binary compatible: once the executable has been generated you could look at it and tell if they are such. Just it (the compiler) doesn't give us these information.
As far as I know any C++ compiler ever existed treats data in a consistent way. I can't even imagine of a compiler generating different representations for those two structures. The point that bugs me the most is that not only those simple A
and B
structs are binary compatible, but about any container is, if you use it with types you can expect to be binary compatible (I ran some tests with GCC 4.5 and Clang 2.8 on both custom containers and STL/boost ones).
Casting operators allow the compiler do what I'm looking to do, but only with basic types. If you cast an int
as const int
(or an int*
and a char*
), and those two types are binary compatible, the compiler can (most likely will) avoid making a copy of it and just use the same raw bytes.
My idea is then to create a custom object_static_cast
that will check if the object of the type it got, and the object of the type to cast into are binary compatible; if they are it just returns the casted reference, otherwise it'll construct a new object and will return it.
Hope to not be downvoted too much for this answer; I'll delete it if SO community doesn't like it.
To check if two types are binary compatible introduced a new type trait:
// NOTE: this function cannot be safely implemented without compiler
// explicit support. It's dangerous, don't trust it.
template< typename T1, typename T2 >
struct is_binary_compatible : public boost::false_type{};
as the note sais (and as said earlier) there's no way to actually implement such type trait (just like boost::has_virtual_destructor
, for example).
Then here is the actual object_static_cast
implementation:
namespace detail
{
template< typename T1, typename T2, bool >
struct object_static_cast_class {
typedef T1 ret;
static ret cast( const T2 &in ) {
return T1( in );
}
};
// NOTE: this is a dangerous hack.
// you MUST be sure that T1 and T2 is binary compatible.
// `binary compatible` means
// plus RTTI could give some issues
// test this any time you compile.
template< typename T1, typename T2 >
struct object_static_cast_class< T1, T2, true > {
typedef T1& ret;
static ret cast( const T2 &in ) {
return *( (T1*)& in ); // sorry for this :(
}
};
}
// casts @in (of type T2) in an object of type T1.
// could return the value by value or by reference
template< typename T1, typename T2 >
inline typename detail::object_static_cast_class< T1, T2,
is_binary_compatible<T1, T2>::value >::ret
object_static_cast( const T2 &in )
{
return detail::object_static_cast_class< T1, T2,
is_binary_compatible<T1, T2>::value >::cast( in );
};
And here an usage example
struct Data {
enum { size = 1024*1024*100 };
char *x;
Data( ) {
std::cout << "Allocating Data" << std::endl;
x = new char[size];
}
Data( const Data &other ) {
std::cout << "Copying Data [copy ctor]" << std::endl;
x = new char[size];
std::copy( other.x, other.x+size, x );
}
Data & operator= ( const Data &other ) {
std::cout << "Copying Data [=]" << std::endl;
x = new char[size];
std::copy( other.x, other.x+size, x );
return *this;
}
~Data( ) {
std::cout << "Destroying Data" << std::endl;
delete[] x;
}
bool operator==( const Data &other ) const {
return std::equal( x, x+size, other.x );
}
};
struct A {
Data x;
};
struct B {
Data x;
B( const A &a ) { x = a.x; }
bool operator==( const A &a ) const { return x == a.x; }
};
#include <cassert>
int main( ) {
A a;
const B &b = object_static_cast< B, A >( a );
// NOTE: this is NOT enough to check binary compatibility!
assert( b == a );
return 0;
}
Output:
$ time ./bnicmop
Allocating Data
Allocating Data
Copying Data [=]
Destroying Data
Destroying Data
real 0m0.411s
user 0m0.303s
sys 0m0.163s
Let's add these (dangerous!) lines before main()
:
// WARNING! DANGEROUS! DON'T TRY THIS AT HOME!
// NOTE: using these, program will have undefined behavior: although it may
// work now, it might not work when changing compiler.
template<> struct is_binary_compatible< A, B > : public boost::true_type{};
template<> struct is_binary_compatible< B, A > : public boost::true_type{};
Output becomes:
$ time ./bnicmop
Allocating Data
Destroying Data
real 0m0.123s
user 0m0.087s
sys 0m0.017s
This should only be used in critical points (not to copy an array of 3 elements once in a while!), and to use this stuff we need at least write some (heavy!) test units for all the types we declared binary compatible, in order to check if they still are when we upgrade our compilers.
Besides to be on the safer side, the undefined-behaving object_static_cast
should only be enabled when a macro is set, so that it's possible to test the application both with and without it.
About my project, I I'll be using this stuff in a point: I need to cast a big container into a different one (which is likely to be binary compatible with my one) in my main loop.
const
types as they aren't assignable. For pointer types, why not use the the most general type that you need to store in the container? In general when you cast aT1
to aT2
the result is a different object so a conversion from a container ofT1
to a container ofT2
implies copying the contained elements. You can't avoid this expense. – Teague