C ++: преобразование контейнера в контейнер другого, но совместимого типа - PullRequest
6 голосов
/ 05 января 2011

Мне часто приходится иметь контейнер C (или любой другой класс-оболочку, даже умные указатели) для типа T1, и я хочу преобразовать такие C<T1> в C<T2>, где T2 совместим с T1.

C ++ не позволяет мне напрямую конвертировать весь контейнер, и принудительное использование reinterpet_cast приведет к неопределенному поведению, поэтому мне нужно будет создать новый C<T2>контейнер и заполнить его с C<T1> пунктов, отлитых как T2.Эта операция может быть довольно дорогой, как во времени, так и в пространстве.

Более того, во многих случаях я почти уверен, что принудительное выполнение reinterpret_cast будет хорошо работать с кодом, скомпилированным любым компилятором, когда-либо существующим, например, когдаT2 - это T1 const, или когда T1 и T2 являются указателями.

Существует ли какой-либо чистый и эффективный способ преобразования C<T1> в C<T2>?
Например,оператор container_cast (/ function?), который создает и заполняет C<T2> тогда и только тогда, когда он не будет двоично совместим с C<T1>?

Ответы [ 8 ]

4 голосов
/ 05 января 2011

Более того, во многих случаях я вполне уверен, что форсирование reinterpret_cast будет работать нормально

Держу пари, что это не так.Два контейнера, в которых хранятся разные типы, никогда гарантированно не будут двоично-совместимыми, даже если их содержащиеся объекты являются.Даже если они оказываются двоично-совместимыми в какой-то конкретной версии реализации компилятора, это деталь реализации, которая может меняться от одной второстепенной версии к следующей.неприятные долгие ночи отладки.

Если вы хотите передать такие контейнеры в функцию, просто сделайте функцию шаблоном, чтобы в нее можно было передавать контейнеры произвольного типа.Похоже с классами.В этом и заключается весь смысл шаблонов.

3 голосов
/ 05 января 2011

Помимо всех других вопросов, решаемых другими:

  • преобразование не подразумевает такой же объем памяти (представьте себе операции преобразования ...)
  • потенциальные специализации класса шаблона (контейнер в вашем вопросе, но с точки зрения компилятора контейнер - это просто еще один шаблон), даже если сами типы являются двоично-совместимыми
  • несвязанность разных экземпляров одного и того же шаблона (для общего случая)

В подходе есть основная проблема, которая вообще не является технической. При условии, что яблоко является фруктом, ни контейнер с фруктами не является контейнером с яблоками (как тривиально продемонстрировано), ни контейнер с яблоками не является контейнером с фруктами. Попробуйте положить арбуз в коробку с яблоками!

Переход к более техническим деталям и конкретно к наследованию, когда преобразование даже не требуется (производный объект уже уже является объектом базового класса), если вам было разрешено привести контейнер с производный тип к базовому типу, тогда вы можете добавить недопустимые элементы в контейнер:

class fruit {};
class apple : public fruit {};
class watermelon : public fruit {};
std::vector<apple*> apples = buy_box_of_apples();
std::vector<fruit*> & fruits = reinterpret_cast< std::vector<fruit*>& >(apples);
fruits.push_back( new watermelon() ); // ouch!!!

Последняя строка совершенно верна: вы можете добавить watermelon к vector<fruit*>. Но общий эффект в том, что вы добавили watermelon к vector<apple*>, и при этом вы нарушили систему типов.

Не все, что кажется простым на первый взгляд, на самом деле вменяемое. Это похоже на причину, по которой вы не можете преобразовать int ** в const int **, даже если первая мысль о том, что это должно быть разрешено. Дело в том, что разрешение так нарушило бы язык (в данном случае - правильность):

const int a = 5;
int *p = 0;
int **p1 = &p;       // perfectly fine
const int **p2 = p1; // should this be allowed??
*p2 = &a;            // correct, p2 points to a pointer to a const int
**p1 = 100;          // a == 100!!!

Что возвращает нас к примеру, который вы предоставили в одном из комментариев к другому ответу (чтобы доказать это в общем, я буду использовать вектор и вместо набора, поскольку содержимое набора является неизменным):

std::vector<int*> v1;
std::vector<const int*> &v2 = v1; // should this be allowed?
const int a = 5;
v2.push_back( &a );  // fine, v2 is a vector of pointers to constant int
                     // rather not: it IS a vector of pointers to non-const ints!
*v1[0] = 10;         // ouch!!! a==10
3 голосов
/ 05 января 2011

Почему бы не использовать безопасный способ

C<T1> c1;
/* Fill c1 */
C<T2> c2(c1.begin(), c1.end());

, а затем профиль.Если это оказывается узким местом, тогда вы всегда можете вернуться к своему базовому алгоритму и, возможно, полностью исключить необходимость конвертации.

Полагаться на какое-либо конкретное поведение из reinterpret_cast может не вызвать проблем сейчас, но месяцы или годыотныне это почти наверняка вызовет проблемы с отладкой.

2 голосов
/ 06 января 2011

Хорошо, позвольте мне обобщить все это.

Ваши (правильные!) Ответы говорят, что в 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 должно быть включено только при установленном макросе, чтобы можно было тестировать приложение как с, так и без него.


Что касается моего проекта, я буду использовать этот материал в одной точке: мне нужно отлить большой контейнер в другой (который, вероятно, будет двоично-совместимым с моим) в моем основной цикл.

2 голосов
/ 05 января 2011

Причина, по которой вы не можете разыграть контейнеры, не имеет ничего общего с самими типами.Проблема в том, что вы пытаетесь привести два объекта, которые, с точки зрения компилятора и компоновщика, являются двумя несвязанными классами.

Когда вы делаете, например, C<int> и C<short>,Компилятор генерирует код, подобный следующему:

class C_int_ {
    //...
};

class C_short_ {
    //...
};

Поскольку эти классы явно не связаны, вы не можете их привести.И если вы заставите его (например, с помощью C-броска), и у него есть какие-либо виртуальные функции, вы, вероятно, что-то взорвете.

Вместо этого вам придется делать это вручную, используя цикл.К сожалению.

1 голос
/ 05 января 2011

Абсолютно не гарантируется, что эти контейнеры двоично совместимы и могут быть преобразованы в что-то вроде reinterpret_cast<>.

Например, если контейнер (например, std::vector) хранит данные внутри себя в массиве в стиле C, C<T1> будет содержать массив T1[], тогда как C<T2> будет содержать T2[]. Если теперь T1 и T2 имеют разные размеры (например, T2 имеет больше переменных-членов), память T1[] не может просто интерпретироваться как T2[], так как элементы этих массивов будут расположены в разные позиции.

Так что простое толкование C<T1> памяти как C<T2> не сработает, и необходимо реальное преобразование.

(Более того, могут существовать шаблонные специализации для разных типов, так что C<T1> может выглядеть совершенно иначе, чем C<T2>)

Для преобразования одного контейнера в другой см., Например, этот вопрос или многие другие связанные с ним.

1 голос
/ 05 января 2011

Это вообще сложно.Проблема становится очевидной при рассмотрении специализации шаблона, например, печально известного vector<bool>, реализация которого отличается от vector<int> гораздо большим, чем просто тип аргумента.

0 голосов
/ 05 января 2011

Это действительно сложно для контейнеров.Совместимости типов недостаточно, типы в действительности должны быть идентичными в памяти, чтобы предотвратить разделение при назначении.Может быть возможно реализовать ptr_container, который предоставляет указатели совместимого типа.Например, ptr_containers boost в любом случае сохраняет void* s внутри, поэтому приведение их к совместимым указателям должно работать.

Тем не менее, это определенно возможно с помощью интеллектуальных указателей.Например, boost::shared_ptr реализует static_pointer_cast и dynamic_pointer_cast.

...