65.9K
CodeProject 正在变化。 阅读更多。
Home

如何确保使用智能指针的自定义删除器正确地跨越动态库边界

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (25投票s)

2013 年 5 月 22 日

MIT

4分钟阅读

viewsIcon

57883

downloadIcon

563

使用智能指针的自定义删除器的解决方案

引言

许多著名的 C++ 专家提倡使用智能指针来代替原始指针,甚至宣称在现代 C++ 中,应该看不到关键字 new 的显式使用。所有的动态内存分配都应该被标准库隐藏,无论是使用像 std::vector 这样的容器,还是使用智能指针。

标准库的智能指针可以自定义它们处理所持有的内存的释放方式。 正如本文所建议的那样,此功能是实现优雅边界跨越的关键。

背景

当一个对象在一个程序集中实例化,并被另一个程序集使用时,就说该对象跨越了动态库边界。 常见的发生方式是,类似工厂的结构实例化对象,并在动态链接库中返回指向它们的指针。

例如,假设另一个库或一个可执行文件链接到该库,并使用该工厂来动态实例化并检索指向对象的指针。 使用该指针的程序集可以对它做任何事情,包括删除该指针以释放它指向的内存。 如果分配内存的库和使用该指针的程序集使用不同版本的动态内存分配 OS 运行时库(在 Windows 上称为 CRT),则内存分配记账将会出现问题。 对于 Microsoft 针对该问题的具体示例,请参阅这里

通常,在 C++11 出现之前,库开发人员必须提供用于释放在其库边界内分配的对象的函数,以避免此问题。 这会产生不良的副作用,即此类库的接口会更重,并且需要特定于每个库的专业知识才能正确分配和释放库的对象。

理想的情况是用户不需要了解分配/释放方案。 他们只需调用库的分配机制(例如,一个工厂),甚至不用担心释放。

Using the Code

与本文相关的代码分为两个项目。 第一个项目是 ExecutableConsumer,这是一个简单的 main 文件,它使用库的工厂来实例化来自库的对象。 第二个项目是 LibraryFactory,它说明了一个有问题的状况和解决方案。

有问题的状况是一个工厂(称为 ProblematicFactory),它实例化一个对象并返回指向它的原始指针。 解决方案是另一个工厂(称为 SafeFactory),它实例化一个对象并返回指向它的 std::unique_ptr,并正确设置其自定义删除器,以便在 DLL 中完成释放。

如果在 Visual Studio 的调试模式下运行程序,并定义了宏 USE_PROBLEMATIC_FACTORY_AND_CAUSE_HEAP_CORRUPTION,您将能够看到调试器检测到堆损坏。

请注意,解决方案中提供的项目有意链接到不同版本的 CRT,以说明堆损坏问题。

因为代码胜过千言万语,所以以下部分将主要包含来自附加文件的带有教学性注释的代码。

可执行文件的 Main 文件

请注意,通过使用大括号在 main 中创建上下文,以封装各个示例。 请记住,在上下文退出时,所有局部变量都会被销毁。

#include <ProblematicFactory.h>
#include <SafeFactory.h>

// change this undef to a define to see the heap corruption assert in debug
#undef USE_PROBLEMATIC_FACTORY_AND_CAUSE_HEAP_CORRUPTION

int main()
{
#ifdef USE_PROBLEMATIC_FACTORY_AND_CAUSE_HEAP_CORRUPTION
  {
    // this allocation is done in the DLL
    auto wMyObject = ProblematicFactory::create();
    // this deallocation is done in the current assembly
    delete wMyObject;
    // when the DLL and this assembly are linked with the exact same CRT DLL,
    // the delete will work properly, otherwise, it will cause heap corruption.
  }
#endif

  {
    // always use auto when possible!
    auto wMyObject = SafeFactory::create();
    // When the program will hit the following curly brace, 
    // wMyObect will be automatically deleted
    // using the custom deleter provided in MyClass.h. 
    // No need to send it back to a de-allocation
    // function of the library!
  }

  {
    // std::unique_ptr can be automatically promoted to a std::shared_ptr
    // and the custom deleter follows, feels like magic!
    std::shared_ptr< MyClass > wMyObject = SafeFactory::create();
    // Same behavior as the example above, 
    // since the shared count will reach zero on the following
    // curly brace.
  }
 
  return 0;
}

库的有问题工厂

这是工厂的典型实现,它返回指向它可以创建的对象的原始指针。

#pragma once

#include "DllSwitch.h"
#include "MyClass.h"

class ProblematicFactory
{
public:
  static LIBRARYFACTORY_API MyClass * create();

private:
  ProblematicFactory();
};

库的安全工厂

从语法上讲,使用此工厂与使用有问题的工厂基本相同(请参见 main 文件),但它将原始指针封装在 std::unique_ptr 中。

#pragma once

#include "DllSwitch.h"
#include "MyClass.h"

#include <memory>

class SafeFactory
{
public:  
  // Note that this function will not be not part of the DLL, so no std::unique_ptr
  // crosses a library boundary! It is going to be built in the client's translation
  // units, therefore it uses the std::unique_ptr of the client. Also note that there
  // is no need to explicitly provide a custom deleter, since a specialization of
  // std::default_delete exists for the class MyClass (see MyClass.h).
  inline static std::unique_ptr< MyClass > create()
  {
    return std::unique_ptr< MyClass >(doCreate());
  }

private:
  SafeFactory();

  static LIBRARYFACTORY_API MyClass * doCreate();
};

库的跨越边界的对象

请注意,此文件中的 default_delete 类是 std 类的特化,因此它需要在 std 命名空间中。

#pragma once
 
#include "DllSwitch.h"

#include <memory>

class LIBRARYFACTORY_API MyClass
{
};

// The following is a specialization of default_delete used by unique_ptr
// for the class MyClass. You need this for all types that the factory can create.
template<>
class LIBRARYFACTORY_API std::default_delete< MyClass >
{
public:
  void operator()(MyClass *iToDelete);
};

关注点

拥有 std 函数的特化起初看起来可能很奇怪,但这是针对您的类的特化。 对于某些 std 模板,这完全合法,并且 std::default_delete 是特化的完美候选者。 如果您感兴趣,请查阅此帖子,它解决了是否可以以及不可以在 std 命名空间中特化的问题。

根据下面的评论,我在库中添加了 MyClassPluginBuilder,以便可以从像插件一样在运行时打开库的可执行文件实例化 MyClass

历史

  • v1.0 - 2013 年 5 月 20 日:首次发布
  • v1.1 - 2014 年 8 月 26 日:添加了 MyClassPluginBuilderExecutableConsumerPlugin
© . All rights reserved.