当基类被标记为虚拟时,普通阻挡后的损坏

问题描述

在正常块断言后,我看到一个奇怪的损坏,当我将基类设为非虚拟时,它消失了。

我已经将范围缩小到进行实际删除调用的那一刻(原始来源具有智能指针,但我已经通过简单的 new/delete 追踪到这种情况也会发生)。

奇怪的情况是,只有在基类被标记为virtual时才会出现这种情况;删除基类的虚拟声明可以防止此错误。我知道虚拟基类会影响构造函数和析构函数的执行顺序,但由于我的类只有一个基类(它是一个充当接口的抽象类),我认为在这种情况下初始化顺序实际上并没有改变。

“界面”:

class INetworkSender
{
public:
    /// <summary>
    /// Finalizes an instance of the <see cref="INetworkSender"/> class.
    /// </summary>
    virtual ~INetworkSender() {}

    /// <summary>
    /// Sends the specified data.
    /// </summary>
    /// <param name="data">The pointer to the buffer holding the data to send.</param>
    /// <param name="length">The amount of data to send.</param>
    virtual void send(const char* data,size_t length) = 0;

    /// <summary>
    /// Returns the remote address.
    /// </summary>
    virtual const std::string& address() const = 0;

    /// <summary>
    /// Returns the remote port.
    /// </summary>
    virtual uint16_t port() const = 0;
};

具体实现类:

class UdpSender
    : virtual public INetworkSender
{
public:
    /// <summary>
    /// Initializes a new instance of the <see cref="UdpSender" /> class.
    /// </summary>
    /// <param name="address">The address to which to send.</param>
    /// <param name="port">The port to which to send.</param>
    UdpSender(const std::string& address,uint16_t port);

    /// <summary>
    /// Finalizes an instance of the <see cref="UdpSender" /> class.
    /// </summary>
    virtual ~UdpSender();

    /// <summary>
    /// Sends the specified data.
    /// </summary>
    /// <param name="data">The pointer to the buffer holding the data to send.</param>
    /// <param name="length">The amount of data to send.</param>
    virtual void send(const char* data,size_t length) override;

    /// <summary>
    /// Returns the remote address.
    /// </summary>
    virtual const std::string& address() const override { return this->m_address; }

    /// <summary>
    /// Returns the remote port.
    /// </summary>
    virtual uint16_t port() const override { return this->m_port; }

private:
    /// <summary>
    /// The socket handle. Note we use a void* to abstract away different OS specific implementations!
    /// </summary>
    void* m_handle;

    /// <summary>
    /// The target address.
    /// </summary>
    std::string m_address;

    /// <summary>
    /// The target port.
    /// </summary>
    uint16_t m_port;
};

最后是触发断言的代码

    UdpSender* sender = new UdpSender("0.0.0.0",0);
    delete sender;

为了确保这不是我在构造函数、析构函数或任何被调用方法中所做的任何事情,我已经注释掉了类实现中的所有内容

UdpSender::UdpSender(const std::string& address,uint16_t port)
    : m_address(address),m_port(port)
{
    /*
    */
}

UdpSender::~UdpSender()
{
    /*
    */
}


void UdpSender::send(const char* data,size_t length)
{
    /*
    */
}

我是否偶然发现了一个晦涩的编译器错误? VS2019 (16.10.0),工具集v142,编译为ISO C++17标准。

编辑:使用地址清理程序提供了更多信息:

==8416==错误:AddressSanitizer:线程 T1 中 0x12135a727100 上的新删除类型不匹配: 传递给 delete 的对象类型错误: 分配类型的大小:72 字节; 释放类型的大小:80 字节。

Edit2:相同但使用非虚拟基类仍然会触发 AddressSanitizer,所以我认为它与类是否为虚拟没有直接关系,尽管它似乎确实影响了内存中对象的大小:

==18300==错误:AddressSanitizer:线程 T1 中 0x12746dba3020 上的新删除类型不匹配: 传递给 delete 的对象类型错误: 分配类型的大小:60 字节; 释放类型的大小:64 字节。

解决方法

地址清理器报告的大小差异最终让我找到了问题的根本原因。

在代码库的某个地方,我通过以下代码使用了一个带有自定义包装边界(4;默认为 8)的结构:

#pragma pack(4)
struct Foo { ... }

包含此结构的头文件的任何代码都将在此之后调整任何结构/类的大小(4 字节边界)与不包含头文件或在结构/类定义(8 字节边界)后包含头文件的代码导致结构/classes 具有两种不同大小的相同结构/类。

显然,包装应该仅用于使用(我想象的特定于 MSVC)实现的特定结构:

#pragma pack(push)
#pragma pack(4)
struct Foo { ... }
#pragma pack(pop)