std::vector 的 C 风格转换

C-style cast of std::vector

我在尝试找到将 std::vector<Derived *> 转换为 std::vector<Base *> 的解决方案时,在现有代码库中偶然发现了这个实现。我正在使用 C++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;
}

感觉非常狡猾,所以我试图找到一个替代方案。 This post 建议使用 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 风格转换有什么缺点?

谢谢。

Why does the first snippet even compile but using a static_cast causes a compilation error?

因为 C 风格的转换是一把大锤,会把所有的谨慎都抛到九霄云外。它的座右铭是 "you want it? you got it",不管 "it" 是什么。静态转换只会执行在静态类型检查方面正确的转换。

What is the computational cost of the two methods? I expect the second one to incur into extra costs due to creating the temporary vector object new_vec, but I am not sure.

您的期望是正确的。但是具有明确语义的代码的成本可能会增加程序的工作量。

What are the drawbacks of using the C-style cast in these cases?

它将始终编译,并且在您将来在某些平台上尝试 运行 之前,您不会发现问题。因为它今天可能有效。

  1. 在 C++ 中有很多情况,规范允许编译器不给出错误,但生成的程序的行为是未定义的。 C 风格的转换很大程度上是 C 遗产遗留下来的遗留兼容性,并且在很多情况下会调用未定义的(通常是损坏的)行为。
  2. 理论上编译器可以优化它,但很可能是的,它会产生一些计算成本。它可能比例如小调用所有这些对象的开销,您可能会在转换它们之后执行这些操作。
  3. C 风格转换的缺点是它不会阻止您调用未定义的行为,并且不会明确说明您的意图(例如 auto x = (Foo) someConstType,您是否打算删除一个const 预选赛还是偶然的?)。

在您的特定情况下,如果您有多重继承,C 风格版本将产生不正确的程序,向上转换指针意味着它的地址需要更改以指向适当的基础 class 对象.

那个代码是胡说八道。没有要求 Derived*valueBase*value 相同,所以告诉编译器假装 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;
}

A std::vector<Derived*> 是与 std::vector<Base*> 无关的类型。没有合法的方法可以将一个人的记忆解释为另一个人的记忆,除了一些疯狂愚蠢的东西,比如新的位置。

如果幸运的话,您的尝试会产生错误。如果你不是,它们会产生未定义的行为,这意味着它今天可能看起来有效,但明天它们可能会由于编译器升级、远距离更改代码或月相等原因悄悄格式化你的硬盘。

现在,vector<Base*> 上的许多操作都适用于 vector<Derived*>。我们可以用 type erasure.

来处理这个问题

这里是低效率类型的擦除class:

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 ); }
};

现在这是一个小玩具,因为它不支持迭代。 (迭代器和我上面写的容器一样复杂)。

Live example.

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 );
}

允许将子容器视为基列表的语言基本上会自动执行上述操作。容器本身具有等效的虚函数 table,或者当您将容器视为视图以建立虚函数时 table 被合成并由客户端代码使用。

以上并不是最有效的;请注意,每个 std::function 都是无国籍的。我可以很容易地用函数指针替换它们,并根据类型 C 存储静态 vtable 来节省内存(但添加另一个间接寻址)。

我们还可以使用非视图类型来更简单地完成此操作,因为我们可以使用类型擦除模型概念模式而不是此手动 vtable 模式。