C ++:将容器转换为不同但兼容类型的容器

时间:2011-01-05 15:14:38

标签: c++ casting containers

我经常遇到类型为C的容器T1(或任何类型的包装类,甚至是智能指针),并希望将此类C<T1>转换为{ {1}},其中C<T2>T2兼容。

C ++不允许我直接转换整个容器,强制T1会导致未定义的行为,所以我需要创建一个新的reinterpet_cast容器并用{{{{}重新填充它1}}项目已投放为C<T2>。无论是在时间和空间上,这项操作都非常昂贵。

此外,在很多情况下,我非常确定强制C<T1>可以正常使用任何编译器编译的代码,例如当T2reinterpret_cast时,或者T2T1 const是指针。

是否有一种干净有效的方法来转换T1中的T2? 例如C<T1>运算符(/ function?),当且仅当它与C<T2>不是二进制兼容时才会创建并重新填充container_cast

8 个答案:

答案 0 :(得分:4)

  

此外,在很多情况下,我非常确定强制reinterpret_cast可以正常工作

我打赌你没有。存储不同类型的两个容器永远不会保证二进制兼容,即使它们包含的对象是。即使它们在某些特定版本的某些编译器实现中碰巧是二进制兼容的,这也是一个可以从一个次要版本更改为下一个版本的实现细节。

依靠这种无证的行为为许多令人不愉快的长夜调试打开了大门。

如果要将这些容器传递给函数,只需将该函数作为模板,以便可以将任意类型的容器传递给它。与班级相似。毕竟,这是模板的重点。

答案 1 :(得分:3)

为什么不使用安全的方式

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

然后是个人资料。如果它成为瓶颈,那么您可以随时重新访问基础算法,并且可能完全不需要转换。

依赖reinterpret_cast的任何特定行为现在可能不会引起问题,但从现在开始数月或数年,它几乎肯定会导致某人调试问题。

答案 2 :(得分:3)

除了其他人处理的所有其他问题:

  • 转换并不意味着相同的内存占用(想想转换操作......)
  • 模板类的潜在特化(问题中的容器,但从编译器的角度看容器只是另一个模板),即使类型本身是二进制兼容的
  • 同一模板的不同实例的无关性(针对一般情况)

该方法存在一个基本问题,根本不是技术问题。如果苹果是水果,水果容器既不是苹果的容器(通常是展示的),也不是苹果的容器是水果的容器。尝试将西瓜放在一盒苹果中!

如果允许转换容器,那么转到更多技术细节,并专门处理甚至不需要转换的继承(派生对象 已经是基类的对象)如果派生类型为基类型,则可以向容器添加无效元素:

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正确性):

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 :(得分:2)

您无法转换容器的原因与类型本身无关。问题是你正在尝试构建两个对象,就编译器和链接器而言,它们是两个不相关的类。

例如,当您执行C<int>C<short>时,编译器会发出如下代码:

class C_int_ {
    //...
};

class C_short_ {
    //...
};

由于这些类显然不相关,因此无法强制转换它们。如果你强迫它(例如,使用C演员),并且它有任何虚拟功能,你可能会爆炸。

相反,您必须使用循环手动完成。遗憾。

答案 4 :(得分:2)

好的,让我总结一下。

你的(正确的!)答案说,在C ++ 二进制兼容性 * 从不保证不同的类型。获取变量所在的内存区域的值是未定义的行为,并将其用于不同类型的变量(对于同一类型的变量,这种情况也应该避免)。

同样在现实生活中,即使对于简单的对象,这个东西也可能是危险的,不要介意容器!

*:通过二进制兼容性我的意思是相同的值以相同的方式存储在内存中,并且使用相同的汇编指令以相同的方式来操作它。例如:即使floatint各为4个字节,它们也不是二进制兼容


但是我不满意这个C ++ 规则:让我们关注一个案例,就像这两个结构一样:struct A{ int a[1000000]; };struct B{ int a[1000000]; };

我们不能只使用A对象的地址,就好像它是B对象一样。这让我很沮丧,原因如下:

  • 编译器静态地知道这些结构是否与二进制兼容:一旦生成了可执行文件,您就可以查看它并告诉它们是否是这样的。只是它(编译器)没有给我们这些信息。

  • 据我所知,任何曾经存在的C ++编译器都以一致的方式处理数据。我甚至无法想象编译器为这两个结构生成不同的表示。最让我烦恼的一点是,不仅那些简单的AB结构是二进制兼容的,而且关于任何容器,如果你将它与类型一起使用,你可以期望二进制兼容(我在自定义容器和STL / boost上运行了GCC 4.5和Clang 2.8的一些测试)。

  • 转换运算符允许编译器执行我想要执行的操作,但仅限于基本类型。如果您将int转换为const int(或int*char*),并且这两种类型二进制兼容,则编译器可以(很可能会)避免复制它并只使用相同的原始字节。


我的想法是创建一个自定义object_static_cast,它将检查它所获得的类型的对象,以及要转换为的类型的对象二进制兼容;如果它们只是返回铸造的引用,否则它将构造一个新对象并将返回它。

希望不要因为这个答案而过分投票;如果SO社区不喜欢它,我会删除它。

要检查两种类型二进制兼容是否引入了新类型特征:

// 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{};

作为注释sais(如前所述),没有办法实际实现这种类型特征(例如,就像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,这样就可以在有和没有它的情况下测试应用程序。


关于我的项目,我将在某一点上使用这些东西:我需要将一个大容器转换为另一个容器(可能是二进制兼容与我的一个)我的主循环。

答案 5 :(得分:1)

这通常很难。在考虑模板特化时,问题变得明显,例如臭名昭​​着的vector<bool>,其实现与vector<int>的区别不仅仅是参数类型。

答案 6 :(得分:1)

绝对不能保证这些容器是二进制兼容的,并且可以使用类似reinterpret_cast<>的内容。

例如,如果容器(如std::vector)将数据内部存储在C样式数组中,C<T1>将包含T1[]数组,而C<T2>将包含T2[]数组一个T1。如果现在T2T2具有不同的大小(例如T1[]包含更多成员变量),则T2[]的内存不能简单地解释为C<T1>因为这些数组的元素将位于不同的位置。

因此,简单地将C<T2>内存解释为C<T1>将无效,并且必须进行真正的转换。

(此外,可能存在针对不同类型的模板特化,因此C<T2>可能看起来与{{1}}完全不同)

要将一个容器转换为另一个容器,请参阅this question或许多其他相关容器。

答案 7 :(得分:0)

这对容器来说确实很困难。类型兼容性不够,类型实际上需要在内存中相同,以防止在分配时切片。有可能实现一个公开兼容类型指针的ptr_container。例如,boost的ptr_containers无论如何都会在内部保留void*,因此将它们转换为兼容的指针应该可以正常工作。

也就是说,智能指针绝对可以实现。例如,boost::shared_ptr实现了static_pointer_castdynamic_pointer_cast