Хорошо, позвольте мне обобщить все это.
Ваши (правильные!) Ответы говорят, что в C ++ двоичная совместимость * не гарантируется * для разных типов. Это неопределенное поведение - брать значение области памяти, в которой находится переменная, и использовать ее для переменной другого типа (и этого, скорее всего, следует избегать и для переменных того же типа).
Также в реальной жизни это может быть опасно даже для простых объектов, не говоря уже о контейнерах!
*: бинарная совместимость Я имею в виду, что одни и те же значения хранятся в памяти одинаковым образом и что одни и те же инструкции по сборке используются одинаковым образом для его манипулирования. Например: даже если float
и int
по 4 байта каждый, они не будут двоично-совместимыми .
Однако я не удовлетворен этим правилом C ++ : давайте сосредоточимся на одном случае, как на этих двух структурах: struct A{ int a[1000000]; };
и struct B{ int a[1000000]; };
.
Мы не можем просто использовать адрес объекта A
, как если бы он был B
. И это расстраивает меня по следующим причинам:
Компилятор статически знает, являются ли эти структуры двоично-совместимыми : после того, как исполняемый файл сгенерирован, вы можете посмотреть на него и сказать, так ли это. Просто он (компилятор) не дает нам этой информации.
Насколько мне известно, когда-либо существовавший компилятор C ++ обрабатывает данные последовательным способом. Я даже не могу представить себе компилятор, генерирующий разные представления для этих двух структур. Больше всего меня беспокоит то, что не только эти простые структуры A
и B
являются бинарно-совместимыми , но и в отношении любого контейнера, если вы используете его с типами, которые вы можете ожидать бинарная совместимость (я провел несколько тестов с GCC 4.5 и Clang 2.8 как на пользовательских контейнерах, так и на STL / boost).
Операторы приведения позволяют компилятору делать то, что я ищу, но только с базовыми типами. Если вы приведете int
к const int
(или int*
и char*
), и эти два типа будут двоично-совместимыми , компилятор может (скорее всего, будет) избегать создания копии и просто используйте те же необработанные байты.
Моя идея состоит в том, чтобы затем создать пользовательский object_static_cast
, который будет проверять, являются ли объект типа, который он получил, и объект типа, в который выполняется приведение, двоично-совместимым ; если это так, он просто возвращает приведенную ссылку, в противном случае он создаст новый объект и вернет его.
Надеюсь, что за этот ответ не понравится слишком много; Я удалю его, если такому сообществу это не понравится.
Чтобы проверить, являются ли два типа двоичными, совместимыми введена новая черта типа:
// 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{};
Как отмечается в примечании (и, как сказано выше), на самом деле нет способа реализовать такую черту типа (как, например, boost::has_virtual_destructor
).
Тогда вот фактическая реализация object_static_cast
:
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 );
};
А вот пример использования
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;
}
Выход:
$ time ./bnicmop
Allocating Data
Allocating Data
Copying Data [=]
Destroying Data
Destroying Data
real 0m0.411s
user 0m0.303s
sys 0m0.163s
Давайте добавим эти (опасные!) Строки перед 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{};
Вывод становится:
$ time ./bnicmop
Allocating Data
Destroying Data
real 0m0.123s
user 0m0.087s
sys 0m0.017s
Это должно использоваться только в критических точках (не копировать массив из 3 элементов время от времени!), И чтобы использовать этот материал, нам нужно по крайней мере написать несколько (тяжелых!) Тестовых модулей для всех объявленных нами типов двоично-совместимая , чтобы проверить, что они все еще , когда мы обновляем наши компиляторы.
Кроме того, чтобы быть более безопасным, поведение с неопределенным поведением object_static_cast
должно быть включено только при установленном макросе, чтобы можно было тестировать приложение как с, так и без него.
Что касается моего проекта, я буду использовать этот материал в одной точке: мне нужно отлить большой контейнер в другой (который, вероятно, будет двоично-совместимым с моим) в моем основной цикл.