std :: vector

时间:2017-09-20 16:09:15

标签: c++ c c++11 casting static-cast

在尝试查找将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风格演员有什么缺点?

感谢。

4 个答案:

答案 0 :(得分:3)

  

为什么第一个片段甚至编译但使用static_cast会导致编译错误?

因为C型演员是一把大锤,会对风大加注意。它的座右铭是&#34;你想要它吗?你得到它&#34;,不管是什么&#34;它&#34;是。静态强制转换只会在静态类型检查方面进行正确的强制转换。

  

这两种方法的计算成本是多少?我希望第二个因为创建临时矢量对象new_vec而产生额外成本,但我不确定。

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

  

在这些情况下使用C风格演员有什么缺点?

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

答案 1 :(得分:2)

那段代码是无稽之谈。不要求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; } 。实际上,如果您有多个相同类型的基数,那么指针类型双关语是不可能的。试试吧:

{{1}}

答案 2 :(得分:1)

  1. 在C ++中有很多情况,规范允许编译器不给出错误,但是结果程序的行为未定义。 C风格的演员阵容在很大程度上是C传统的遗留兼容性,并且在很多情况下会调用未定义的(通常是破坏的)行为。
  2. 从理论上讲,编译器可以对其进行优化,但最有可能的是,它会产生一些计算成本。它可能小于例如调用所有这些对象的开销,你可能会在投射它们之后做的。
  3. C风格的演员有一个缺点,就是它不会阻止你调用未定义的行为,并且没有明确你的意图(例如auto x = (Foo) someConstType,你的意思是删除const限定词或者是偶然的?)。
  4. 在您的特定情况下,如果您有多个继承,C样式版本将产生不正确的程序,并且向上转换指针意味着其地址需要更改为指向适当的基类对象。

答案 3 :(得分:1)

std::vector<Derived*>是与std::vector<Base*>无关的类型。没有合法的方法可以将一个人的记忆解释为另一个,而不是像放置新事物一样疯狂的愚蠢。

如果幸运的话,你的尝试会产生错误。如果你不是,它们会产生未定义的行为,这意味着它似乎可以在今天工作,但明天它们可以静默格式化你的硬盘驱动器,因为从编译器升级到代码远离改变,或月亮的阶段。 / p>

现在,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 ); }
};

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

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

允许将子容器视为基础列表的语言基本上自动执行上述类型的操作。容器本身具有等效的虚函数表,或者当您将容器视为基于虚拟功能表的视图时,客户端代码将合成并使用它。

以上效率不是最高;请注意,每个std::function都是无国籍的。我可以很容易地用函数指针替换它们,并且基于类型C存储静态vtable而不是节省内存(但添加其他间接)。

我们也可以使用非视图类型更简单一点,因为我们可以使用类型擦除模型 - 概念模式而不是这种手动vtable模式。

相关问题