Приведение std::vector в стиле C

Я наткнулся на эту реализацию в существующей кодовой базе, когда пытался найти решение для приведения std::vector<Derived *> к std::vector<Base *>. Я использую С++11.

Рассмотрим следующий фрагмент кода:

#include <iostream>
#include <vector>

class A
{
    // some implementation details
};

class B : public A
{
    // some implementation details
};

void count(std::vector<A *> const & a_vec)
{
  std::cout << "IT HAS THESE MANY PTRS: " << a_vec.size() << std::endl;
}

int main()
{
  B * b;

  std::vector<B *> b_vec {b};
  count((std::vector<A *> &) b_vec);

  return 0;
}

Это казалось очень изворотливым, и поэтому я попытался найти альтернативу. В этом сообщении предлагается подход с использованием std::vector::assign. Итак, теперь моя основная функция будет выглядеть так:

int main()
{
  B * b;

  std::vector<B *> b_vec {b};
  std::vector<A *> new_vec;
  new_vec.assign(b_vec.begin(), b_vec.end());
  count(new_vec);

  return 0;
}

Он компилируется и работает так, как ожидалось. Теперь у меня следующие вопросы:

1) Почему первый фрагмент даже компилируется, но использование static_cast вызывает ошибку компиляции?

2) Какова вычислительная стоимость двух методов? Я ожидаю, что второй повлечет за собой дополнительные расходы из-за создания временного векторного объекта new_vec, но я не уверен.

3) Каковы недостатки использования приведения в стиле C в этих случаях?

Спасибо.


person antogilbert    schedule 20.09.2017    source источник
comment
1) То, что что-то компилируется, не означает, что оно будет работать или даст правильные результаты. 2) Делать что-то правильно обычно труднее, чем использовать незаконные ярлыки. 3) Недостаток в том, что это неопределенное поведение.   -  person Sam Varshavchik    schedule 20.09.2017
comment
Приведение в стиле C говорит компилятору «Просто заткнись и делай это!». Это позаботится об ошибке или предупреждающем сообщении, но затем вам решать, действительно ли это работает. Компилятор не скажет вам, поскольку вы только что попросили этого не делать.   -  person Bo Persson    schedule 20.09.2017
comment
Полиморфизм времени выполнения и STL плохо сочетаются. Это альтернатива, boost.org/doc/libs /1_65_1/libs/ptr_container/doc/   -  person alfC    schedule 20.09.2017


Ответы (4)


Почему первый фрагмент даже компилируется, но использование static_cast вызывает ошибку компиляции?

Потому что бросок в стиле C — это кувалда, которая отбросит всякую осторожность на ветер. Его девиз: «Хочешь? Получишь», независимо от того, что «это» есть. Статическое приведение будет выполнять только правильное приведение с точки зрения проверки статического типа.

Какова вычислительная стоимость двух методов? Я ожидаю, что второй повлечет за собой дополнительные расходы из-за создания временного векторного объекта new_vec, но я не уверен.

Ваше ожидание верно. Но стоимость кода с четко определенной семантикой может заключаться в дополнительной работе программы.

Каковы недостатки использования приведения в стиле C в этих случаях?

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

person StoryTeller - Unslander Monica    schedule 20.09.2017

Этот код ерунда. Не требуется, чтобы значение Derived* было таким же, как значение Base*, поэтому компилятору не требуется притворяться, что std::vector<B*> является std::vector<A*>. ни к чему разумному. Фактически, этот каламбур типа указателя невозможен, если у вас есть несколько баз одного и того же типа. Попытайся:

#include <iostream>

struct Base {
    int i;
};

struct I1 : Base {
    int j;
};

struct I2 : Base {
    int k;
};

struct Derived : I1, I2 {
    int l;
};

int main() {
    Derived d;
    Base* b1 = &(I1&)d;
    Base* b2 = &(I2&)d;
    std::cout << (void*)&d << ' ' << (void*)b1 << ' ' << (void*)b2 << '\n';
    return 0;
}
person Pete Becker    schedule 20.09.2017

  1. В C++ есть много случаев, когда спецификация позволяет компилятору не выдавать ошибку, но поведение полученной программы не определено. Приведения в стиле C в значительной степени являются устаревшей совместимостью, оставшейся от наследия C, и во многих случаях вызывают неопределенное (часто нарушенное) поведение.
  2. Теоретически компилятор может оптимизировать его, но, скорее всего, да, это повлечет за собой некоторые вычислительные затраты. Это, вероятно, меньше, чем, например. накладные расходы на вызов всех этих объектов, которые вы предположительно сделали бы после их приведения.
  3. Приведение в стиле C имеет тот недостаток, что оно не мешает вам вызывать неопределенное поведение и не дает понять, каково ваше намерение (например, с auto x = (Foo) someConstType вы хотели удалить квалификатор const или это было случайно?) .

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

person Josef Grahn    schedule 20.09.2017

std::vector<Derived*> — это тип, не связанный с std::vector<Base*>. Не существует законного способа интерпретировать память одного как другого, за исключением чего-то безумно глупого, например, размещения нового.

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

Так вот, многие операции с vector<Base*> работают с vector<Derived*>. Мы можем справиться с этим с помощью стирания типа.

Вот класс стирания малоэффективного типа:

template<class R, class...Args>
using vcfunc = std::function<R(void const*, Args...)>;

template<class T, class R, class...Args, class F>
vcfunc<R,Args...> vcimpl( F&& f ) {
  return [f=std::forward<F>(f)](void const* pt, Args&&...args)->R{
    return f( *static_cast<T const*>(pt), std::forward<Args>(args)... );
  };
}
template<class T>
struct random_access_container_view {
  using self=random_access_container_view;
  struct vtable_t {
    vcfunc<std::size_t> size;
    vcfunc<bool> empty;
    vcfunc<T, std::size_t> get;
  };  
  vtable_t vtable;
  void const* ptr = 0;
  template<class C,
    class dC=std::decay_t<C>,
    std::enable_if_t<!std::is_same<dC, self>{}, int> =0
  >
  random_access_container_view( C&& c ):
    vtable{
      vcimpl<dC, std::size_t>( [](auto& c){ return c.size(); } ),
      vcimpl<dC, bool>( [](auto& c){ return c.empty(); } ),
      vcimpl<dC, T, std::size_t>( [](auto& c, std::size_t i){ return c[i]; } )
    },
    ptr( std::addressof(c) )
  {}

  std::size_t size() const { return vtable.size( ptr ); }
  bool empty() const { return vtable.empty( ptr ); }
  T operator[](std::size_t i) const { return vtable.get( ptr, i ); }
};

Теперь это немного игрушка, поскольку она не поддерживает итерацию. (Итераторы примерно такие же сложные, как описанный выше контейнер).

Живой пример.

struct A {
    char name='A';
};

struct B:A {
    B(){ name='B'; }
};

void print_them( random_access_container_view<A> container ) {
    for (std::size_t i = 0; i < container.size(); ++i ) {
        std::cout << container[i].name << "\n";
    }
}
int main() {
    std::vector<B> bs(10);
    print_them( bs );
}

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

Вышеупомянутое не максимально эффективно; обратите внимание, что каждый std::function был без гражданства. Я мог бы довольно легко заменить их указателями на функции и сохранить статическую виртуальную таблицу на основе типа C вместо этого для экономии памяти (но добавить другую косвенность).

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

person Yakk - Adam Nevraumont    schedule 20.09.2017