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?
它将始终编译,并且在您将来在某些平台上尝试 运行 之前,您不会发现问题。因为它今天可能有效。
- 在 C++ 中有很多情况,规范允许编译器不给出错误,但生成的程序的行为是未定义的。 C 风格的转换很大程度上是 C 遗产遗留下来的遗留兼容性,并且在很多情况下会调用未定义的(通常是损坏的)行为。
- 理论上编译器可以优化它,但很可能是的,它会产生一些计算成本。它可能比例如小调用所有这些对象的开销,您可能会在转换它们之后执行这些操作。
- C 风格转换的缺点是它不会阻止您调用未定义的行为,并且不会明确说明您的意图(例如
auto x = (Foo) someConstType
,您是否打算删除一个const
预选赛还是偶然的?)。
在您的特定情况下,如果您有多重继承,C 风格版本将产生不正确的程序,向上转换指针意味着它的地址需要更改以指向适当的基础 class 对象.
那个代码是胡说八道。没有要求 Derived*
的 value 与 Base*
的 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 ); }
};
现在这是一个小玩具,因为它不支持迭代。 (迭代器和我上面写的容器一样复杂)。
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 模式。
我在尝试找到将 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?
它将始终编译,并且在您将来在某些平台上尝试 运行 之前,您不会发现问题。因为它今天可能有效。
- 在 C++ 中有很多情况,规范允许编译器不给出错误,但生成的程序的行为是未定义的。 C 风格的转换很大程度上是 C 遗产遗留下来的遗留兼容性,并且在很多情况下会调用未定义的(通常是损坏的)行为。
- 理论上编译器可以优化它,但很可能是的,它会产生一些计算成本。它可能比例如小调用所有这些对象的开销,您可能会在转换它们之后执行这些操作。
- C 风格转换的缺点是它不会阻止您调用未定义的行为,并且不会明确说明您的意图(例如
auto x = (Foo) someConstType
,您是否打算删除一个const
预选赛还是偶然的?)。
在您的特定情况下,如果您有多重继承,C 风格版本将产生不正确的程序,向上转换指针意味着它的地址需要更改以指向适当的基础 class 对象.
那个代码是胡说八道。没有要求 Derived*
的 value 与 Base*
的 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 ); }
};
现在这是一个小玩具,因为它不支持迭代。 (迭代器和我上面写的容器一样复杂)。
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 模式。