代码之家  ›  专栏  ›  技术社区  ›  PeterK

如何在不破坏代码的情况下正确修复“结构/联合中的零大小数组”警告(C4200)?

  •  19
  • PeterK  · 技术社区  · 14 年前

    我正在把一些代码集成到我的库中。它是一个复杂的数据结构,针对速度进行了很好的优化,所以我尽量不要修改太多。集成过程进行得很顺利,实际上几乎完成了(它编译)。有一件事仍然困扰着我。我多次收到C4200警告:

    warning C4200: nonstandard extension used : zero-sized array in struct/union
    Cannot generate copy-ctor or copy-assignment operator when UDT contains a zero-sized array
    

    代码 作品 但是这个警告让我毛骨悚然(尤其是带有copy-ctor的部分)。警告出现的原因是声明的结构如下:

    #pragma pack( push )
    #pragma pack( 1 )
    // String
    struct MY_TREEDATSTR
    {
        BYTE btLen;
        DWORD dwModOff;
        BYTE btPat[0];
    };
    
    typedef MY_TREEDATSTR TREEDATSTR;
    typedef MY_TREEDATSTR *PTREEDATSTR;
    
    #pragma pack( pop )
    

    注意 btPat[0] . 有什么方法可以轻松地 正确地 在不破坏代码和/或更改过多代码的情况下消除此警告。注意到 #pragma 根据这个警告,它们有什么意义吗?为什么这个结构是以这种方式声明的呢?(我指的是 btPat 事情,而不是 α语用 是的,我理解的那些)。

    注:我看到了 this similar question 但这对我没有帮助。

    更新:正如我所说,代码工作并给出正确的结果。因此,显然不需要复制构造函数或赋值运算符。当我看代码的时候,所有的结构都没有得到memcpy-ed。

    6 回复  |  直到 9 年前
        1
  •  13
  •   David Rodríguez - dribeas    10 年前

    我假设你希望这是用纯C++模式编译的,并且你不想只编译C中的一些文件,而不需要在C++和后面的链接中编译。

    警告说明编译器生成的复制构造函数和赋值很可能与您的结构不符。在结构的末尾使用零大小的数组通常是C的一种方式,它在运行时决定了一个数组,但是在C++中是非法的,但是你可以得到1的类似行为:

    struct runtime_array {
       int size;
       char data[1];
    };
    runtime_array* create( int size ) {
       runtime_array *a = malloc( sizeof(runtime_array) + size ); // [*]
       a->size = size;
       return a;
    }
    int main() {
       runtime_array *a = create( 10 );
       for ( int i = 0; i < a->size; ++i ) {
          a->data[i] = 0;
       }
       free(a);
    }
    

    这种类型的结构应该是动态分配的——或者使用动态堆栈分配技巧——并且通常不会被复制,但是如果您尝试过,您会得到奇怪的结果:

    int main() {
       runtime_array *a = create(10);
       runtime_array b = *a;          // ouch!!
       free(a);
    }
    

    在此示例中,编译器生成的复制构造函数将精确地分配 sizeof(runtime_array) 堆栈中的字节,然后将数组的第一部分复制到 b . 问题是 有一个 size 字段表示10,但对任何元素都没有内存。

    如果您仍然希望能够在C语言中编译这个代码,那么您必须通过闭上眼睛来解决这个警告:不要理会那个特定的警告。如果只需要C++兼容性,可以手动禁用复制构造和赋值:

    struct runtime_array {
       int size;
       char data[1];
    private:
       runtime_array( runtime_array const & );            // undefined
       runtime_array& operator=( runtime_array const & ); // undefined
    };
    

    通过声明复制构造函数和赋值运算符,编译器不会为您生成一个(并且不会抱怨它不知道如何生成)。如果您错误地试图在代码中使用这两个私有项,您将得到编译时错误。因为从未调用它们,所以它们可以保持未定义——这也用于避免从类的其他方法中调用它,但我假定没有其他方法。

    由于您正在重构C++,所以我还将使默认构造函数私有,并提供一个静态公共内联方法,该方法将负责内容的正确分配。如果还将析构函数设为私有,则可以确保用户代码不会尝试调用 delete 在您的对象上:

    struct runtime_array {
       int size;
       char data[1];
       static runtime_array* create( int size ) {
          runtime_array* tmp = (runtime_array*)malloc(sizeof(runtime_array)+size);
          tmp->size = size;
          return tmp;
       }
       static void release( runtime_array * a ) {
          free(a);
       }
    private:
       runtime_array() {}
       ~runtime_array() {}
       runtime_array( runtime_array const & );            // undefined
       runtime_array& operator=( runtime_array const & ); // undefined
    };
    

    这将确保用户代码不会错误地在堆栈中创建对象,也不会混合对 malloc/free 打电话给 new/delete ,因为您管理对象的创建和销毁。这些更改都不会影响对象的内存布局。

    [*]此处的大小计算有点偏离,并且将过度分配,可能是 sizeof(int) 因为对象的大小在末尾有填充。

        2
  •  15
  •   Nordic Mainframe    14 年前

    如果这是一个msvc编译器(这是警告消息告诉我的),那么您可以使用pragma warning禁用此警告,即:

    #pragma warning( push )
    #pragma warning( disable : 4200 )
    struct _TREEDATSTR
    {
        BYTE btLen;
        DWORD dwModOff;
        BYTE btPat[0];
    };
    #pragma warning( pop )
    

    顺便说一句,关于复制构造函数的消息并不令人毛骨悚然,但是 好东西 因为这意味着,如果btpat中没有未知字节,就无法复制treedatstr的实例:编译器不知道treedatstr有多大(因为0大小的数组),因此拒绝生成复制构造函数。这意味着,你不能这样做:

    _TREEDATSTR x=y;
    

    无论如何,这不应该起作用。

        3
  •  3
  •   dreamlax    14 年前

    试着换个说法 btPat[1] 相反。我认为C++和C标准都规定数组不能有0个元素。它可能会导致依赖 _TREEDATSTR 结构本身,但通常这些类型的结构都是从缓冲区中进行类型转换的,其中(在本例中)缓冲区的第一个字节决定实际包含多少字节 btPat . 这种方法依赖于这样一个事实:在C数组上没有边界检查。

        4
  •  2
  •   6502    14 年前

    这个C(反)模式的主要思想是为treedatstr元素获取所需的额外内存;换句话说,分配将使用malloc(sizeof(ou treedatstr)+len完成。

    pragma pack用于要求编译器在字段之间不留空格(通常编译器会在结构字段之间留一些未使用的字节以确保对齐,因为在许多现代处理器中,这是一个巨大的速度改进)。

    但是请注意,有些架构中,未对齐的访问不仅速度慢…但是完全禁止(segfault),因此这些编译器可以自由地忽略pragma包;使用pragma包的代码本质上是不可移植的。

    我想我应该先把双字放在结构中,而这可能不需要一个pragma包;另外一种消除警告的方法是分配一个元素数组,并使用(len-1)额外的字节进行分配。

    在C++中,所有这些东西都是相当危险的,因为你基本上是在愚弄编译器,认为对象的大小比实际的要小,并且假定C++是一种复制逻辑语言,这意味着需要麻烦(例如对于只在对象的第一部分起作用的复制构造和赋值函数)。 对于日常使用来说,使用std::vector肯定更好,但这当然会带来更高的代价(双间接,每个树型实例的内存更多)。

    我通常不喜欢认为所有其他程序员都是白痴,所以如果使用了这种糟糕的技巧,那么可能有一个高薪的理由…然而,要做出最终的判断,需要进行更深入的检查。

    总结:

    1. 在数组末尾使用零元素数组是用于创建可变大小对象的技巧。分配是通过请求sizeof(structure)+n*sizeof(array_element)字节完成的。
    2. pragma pack用于告诉编译器避免在结构字段之间添加额外的填充字节。当需要对内存布局进行精确控制时(例如,因为这些对象正由手写的程序集访问),就需要这样做。
    3. 不要在C++中这样做,除非你真的需要它,你知道你在做什么。

    没有办法“正确”地静默警告,因为代码想要玩脏(C++编译器不喜欢被对象大小欺骗)。如果您在其他对象中使用这个对象,或者作为其他对象的基础,或者通过值传递它,那么无论发生什么坏情况,您都需要它。

        5
  •  1
  •   Brian Hooper    14 年前

    如果它在抱怨复制构造函数和赋值运算符函数,那么您不能提供自己的函数吗?如果你不想要他们,把他们宣布为私人。

    如果您在没有意识到的情况下分配或复制代码,这可能会在代码的其他地方产生很多错误,在这种情况下,它无论如何都不会工作,因为没有自动生成的错误。

        6
  •  0
  •   degski    9 年前

    虽然我意识到这是一个老线程,但我想把我的纯C++ 11解决方案提供给OP的问题。其思想是将要分配的对象包装起来,添加填充以将数组中的对象与2个地址的幂对齐,方法如下:

    template<typename T, std::size_t ObjectPaddingSize>
    struct PaddedType : private T { private: char padding [ ObjectPaddingSize ]; };
    
    template<typename T> // No padding.
    struct PaddedType<T, 0> : private T { };
    
    template<typename T>
    struct PaddedT : private PaddedType<T, NextPowerOfTwo<sizeof ( T )>::value - sizeof ( T )> { };
    

    对象填充大小可以在编译时用以下类计算(如果l是2的幂,则返回l,否则返回2的下一次幂gt l):

    template<std::size_t L>
    class NextPowerOfTwo {
    
        template <std::size_t M, std::size_t N>
        struct NextPowerOfTwo1 {
    
            enum { value = NextPowerOfTwo1<N, N & ( N - 1 )>::value };
        };
    
        template <std::size_t M>
        struct NextPowerOfTwo1<M, 0> {
    
            enum { value = M << 1 };
        };
    
        // Determine whether S is a power of 2, if not dispatch.
    
        template <std::size_t M, std::size_t N>
        struct NextPowerOfTwo2 {
    
            enum { value = NextPowerOfTwo1<M, M>::value };
        };
    
        template <std::size_t M>
        struct NextPowerOfTwo2<M, 0> {
    
            enum { value = M };
        };
    
    public:
    
        enum { value = NextPowerOfTwo2<L, L & ( L - 1 )>::value };
    };