【问题标题】:C++: converting a container to a container of different yet compatible typeC++:将容器转换为不同但兼容类型的容器
【发布时间】:2011-06-04 02:05:36
【问题描述】:

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

C++ 不允许我直接转换整个容器,并且强制 reinterpet_cast 会导致未定义的行为,因此我需要创建一个新的 C<T2> 容器并用 C<T1> 项目重新填充它T2。这种操作在时间和空间上都可能非常昂贵。

此外,在许多情况下,我很确定强制使用 reinterpret_cast 将适用于任何存在的编译器编译的代码,例如当 T2T1 const 时,或者当 T1 和 @987654335 时@ 是指针。

是否有任何干净有效的方法可以将C<T1> 转换为C<T2>
例如,container_cast 运算符(/function?)创建并重新填充C<T2> 当且仅当它与C<T1> 二进制不兼容?

【问题讨论】:

  • 大多数标准容器不支持const 类型,因为它们不可分配。对于指针类型,为什么不使用需要存储在容器中的最通用的类​​型呢?通常,当您将T1 转换为T2 时,结果是不同的对象,因此从T1 的容器到T2 的容器的转换意味着复制包含的元素。您无法避免这笔费用。
  • 这种设计存在根本性的缺陷,即需要转换容器。在不同类型上实例化的容器永远不能保证与强制转换兼容,同时它们包含的类型可能兼容也可能不兼容。如果它们兼容,则转换容器持有的对象,而不是容器本身。

标签: c++ casting container-data-type


【解决方案1】:

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

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

class C_int_ {
    //...
};

class C_short_ {
    //...
};

由于这些类显然是不相关的,因此您不能强制转换它们。如果你强制它(例如,使用 C 转换),并且它有任何虚函数,你可能会炸毁一些东西。

相反,您必须使用循环手动完成。对不起。

【讨论】:

  • 我知道 reinterpret_cast 因此不安全。您是否阅读了该问题的第三和第四段?有时类型是二进制兼容的,我正在寻找一种方法让编译器reinterpret_cast 在这种情况下。
  • 是的,我做到了。但是,我要说的是,即使字节碰巧是相同的,它们也是 not 二进制兼容的。而且,你不能做任何事情都无法说服编译器。
  • 当然C<int>C<float> 不同。如果我的情况是这样的,我什至不会打扰自己。我更明确地考虑C<int const*>C<int*>。听说没有编译器会生成不同的代码(除非C 专门针对常量指针)...
  • 要记住的是,编译器 字面上 生成类,就像我在答案中所做的那样。有一次(很久以前,当 C++ 编译器以 C 为目标时),它是在预处理器中完成的。所以,并不是编译器在装傻,而是它们与class A { int a; };class B { int a;}; 完全不同
  • @peoro:问题是没有什么可以保证它们是真正二进制兼容的。你可以猜测,甚至可以执行一些静态断言(两个容器的基本类型是 POD,大小相同,...),但我认为没有任何方法可以 100% 确定。
【解决方案2】:

此外,在许多情况下,我很确定强制 reinterpret_cast 会正常工作

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

依赖这种未记录的行为为许多令人不快的漫长调试之夜打开了大门。

如果您想将此类容器传递给函数,只需将函数设为模板,以便可以将任意类型的容器传递给它。与类类似。毕竟,这就是模板的全部意义所在。

【讨论】:

  • 当然不能保证,但实际上任何编译器都会以相同的方式存储 std::set<int const*>std::set<int*>,我敢打赌你会找到任何真正的编译器或 STL 实现不会做这个。我还认为编译器提供一个非标准扩展(宏)来说明它是否保证两种类型是二进制兼容的应该不难。
  • @peoro:问题有两个:(1)这些容器可能存储运行时类型信息;当然,这在发布模式下不太可能(因为它会产生开销),但在调试时完全可能。 (2) 即使对于具有不同内存布局的二进制兼容类型,也可能存在容器的特化。
  • @peoro:我添加了一个answer,试图解释问题是允许这些类型的转换会以比它可能帮助的更糟糕的方式破坏语言。有一个具体的例子说明为什么即使 std::vector<int*>std::vector<const int*> 是二进制兼容的,但允许转换会破坏语言中的 const 正确性。
【解决方案3】:

这通常是困难的。当考虑模板特化时,问题变得很明显,例如臭名昭​​著的vector<bool>,它的实现与vector<int> 的不同之处远不止参数类型。

【讨论】:

    【解决方案4】:

    绝对不能保证这些容器是二进制兼容的,并且可以使用 reinterpret_cast<> 之类的东西进行转换。

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

    因此,简单地将C<T1> 内存解释为C<T2> 是行不通的,需要进行真正的转换。

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

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

    【讨论】:

    • 好的,如果T1T2 是二进制不兼容的,那么我所要求的将是不可能的。我在考虑像 C<const int*>C<int*> 这样的情况,其中 C hos 没有关于其包含常量的专业化:除非对于非常奇怪的编译器 reinterpret_cast 会正常工作。
    【解决方案5】:

    为什么不使用安全的方式

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

    然后配置文件。如果它被证明是一个瓶颈,那么您可以随时重新审视您的底层算法,或许可以完全消除转换的需要。

    依赖reinterpret_cast 的任何特定行为现在可能不会导致问题,但几个月或几年后它几乎肯定会导致调试问题。

    【讨论】:

      【解决方案6】:

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

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

      【讨论】:

        【解决方案7】:

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

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

        该方法存在一个根本不是技术性的基本问题。假设一个苹果是一种水果,那么一个装水果的容器就不是装苹果的容器(简单演示),装苹果的容器也不是装水果的容器。试着把一个西瓜装进一盒苹果!

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

        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&lt;fruit*&gt;。但最终的结果是您在vector&lt;apple*&gt; 中添加了watermelon,这样做就破坏了类型系统。

        并非所有乍看之下看起来很简单的东西实际上都是正常的。这与您无法将 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!!!
        

        这让我们回到了您在其中一个 cmets 中提供的另一个答案的示例(为了证明这一点,我将使用向量而不是集合,因为集合内容是不可变的):

        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
        

        【讨论】:

        • 嗯,谢谢,您的回答最能告诉我为什么我不应该从逻辑的角度来看,即使它在实践中会起作用。我们会记住你的例子,它们可以回答我脑海中有时会产生的许多疑问。我的情况有点不同(我需要放弃对象:当我给它的函数返回时会破坏它 - 可能是这样的函数设计不好,不知道)。现在我知道为什么我不应该在其他情况下寻求类似的解决方案。
        【解决方案8】:

        好吧,让我总结一下整个事情。

        您的(正确!)答案表明,在 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{};
        

        正如注释所说(如前所述),没有办法实际实现这种类型特征(例如 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 仅应在设置宏时启用,以便可以在有和没有它的情况下测试应用程序。


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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2011-11-13
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2020-05-28
          • 1970-01-01
          相关资源
          最近更新 更多