将自动推论与unique_ptr和自定义删除器一起使用

时间:2018-07-09 18:40:00

标签: c++ c++17 unique-ptr type-deduction

我当前正在使用一个C库,该库定义了许多数据类型,所有这些数据类型都需要由用户管理其生命周期。以这种方式定义了许多功能:

int* create() {
    return new int();
}

void destroy(int* i) {
    delete i;
}

其中大多数不需要在创建后访问。它们只需要存在。因此,我正在尝试使用unique_ptr声明的声明来管理它们,而我需要它们居住。

这是这样的声明:

// Note that I'm avoiding writing the type's name manually.
auto a = std::unique_ptr<std::decay_t<decltype(*create())>, decltype(&destroy)>{create(), &destroy};

但这过于冗长,因此我将其封装在实用程序功能模板中:

template<typename T>
auto make_unique_ptr(T* p, void (*f)(T*)) {
    return std::unique_ptr<T, decltype(f)>(p, f);
}

使用哪种方式:

auto b = make_unique_ptr(create(), &destroy);

这看起来不错,但是引入了一个非标准函数,除了作为某些声明的语法糖之外,它没有实际目的。我的同事甚至可能都不知道它的存在,最终以不同的名称创建了它的其他版本。

介绍了class template argument deduction。我认为这是解决我的问题的完美解决方案:一种无需使用用户定义的包装器即可推断所有这些类型的标准方法。所以我尝试了这个:

auto c = std::unique_ptr{create(), &destroy};

正如C ++编译器和模板的规则一样,此操作失败并显示几行长的错误消息。以下是相关部分:

(...): error: class template argument deduction failed:
 auto c = std::unique_ptr{create(), &destroy};
                                            ^
(...): note: candidate: 'template<class _Tp, class _Dp> 
unique_ptr(std::unique_ptr<_Tp, _Dp>::pointer, typename std::remove_reference<_Dp>::type&&)-> std::unique_ptr<_Tp, _Dp>'
       unique_ptr(pointer __p,
       ^~~~~~~~~~
(...): note:   template argument deduction/substitution failed:
(...): note:   couldn't deduce template parameter '_Tp'
     auto c = std::unique_ptr{create(), &destroy};
                                                ^

理论上,我可以添加一个演绎指南来处理:

namespace std {

template<typename T>
unique_ptr(T* p, void (*f)(T*)) -> unique_ptr<T, decltype(f)>;

}

它至少在我的gcc版本上确实有效,但是该标准不太喜欢它:

  

[namespace.std]

     

1除非另有说明,否则如果C ++程序将声明或定义添加到名称空间std或名称空间std中的名称空间中,则行为是不确定的。

     

4如果声明,则C ++程序的行为是不确定的
  (...)
  4.4-有关任何标准库类模板的推论指南。

还有一些与区分指针和数组有关的问题,但让我们忽略它。

最后,问题:在使用自定义删除器时,是否还有其他方法可以“帮助” std::unique_ptr(或也许std::make_unique)来推断正确的类型?以防万一这是XY问题,对于这些类型的生命周期管理(也许std::shared_ptr),我是否没有想到过任何解决方案?如果这两个答案均否定,那么我应该期待上有什么改进可以解决此问题?

Feel free to test the above examples at Coliru.

4 个答案:

答案 0 :(得分:2)

一种类型作为值:

template<class T>
struct tag_t {};
template<class T>
constexpr tag_t<T> tag{};

一个值作为类型:

template<auto f>
using val_t = std::integral_constant<std::decay_t<decltype(f)>, f>;
template<auto f>
constexpr val_t<f> val{};

请注意,val<some_function>是一个空类型,可以在constexpr上下文中使用()进行调用,它将调用some_function。它也可以存储int或其他任何东西,但是我们将使用它无状态存储函数指针。

现在让我们玩得开心:

namespace factory {
  // This is an ADL helper that takes a tag of a type
  // and returns a function object that can be used
  // to allocate an object of type T.
  template<class T>
  constexpr auto creator( tag_t<T> ) {
    return [](auto&&...args){
      return new T{ decltype(args)(args)... };
    };
  }
  // this is an ADL helper that takes a tag of a type
  // and returns a function object that can be used
  // to destroy that type
  template<class T>
  constexpr auto destroyer( tag_t<T> ) {
    return std::default_delete<T>{};
  }

  // This is a replacement for `std::unique_ptr`
  // that automatically finds the destroying function
  // object using ADL-lookup of `destroyer(tag<T>)`.
  template<class T>
  using unique_ptr = std::unique_ptr< T, decltype(destroyer(tag<T>)) >; // ADL magic here

  // This is a replacement for std::make_unique
  // that uses `creator` and `destroyer` to find
  // function objects to allocate and clean up
  // instances of T.
  template<class T, class...Args>
  unique_ptr<T> make_unique(Args&&...args) {
    // ADL magic here:
    return unique_ptr<T>( creator( tag<T> )(std::forward<Args>(args)...) );
  }
}

好的,那是一个框架。

现在让我们假设您有一些图书馆。它有一个类型。它需要超级秘密的特殊调味料才能在此处创建和销毁其实例:

namespace some_ns {
  struct some_type {
    int x;
  };
  some_type* create( int a, int b ) {
    return new some_type{ a+b }; // ooo secret
  }
  void destroy( some_type* foo ) {
    delete foo; // ooo special
  }
}

,我们想将其连接起来。您重新打开名称空间:

namespace some_ns {
  constexpr auto creator( tag_t<some_type> ) {
    return val<create>;
  }
  constexpr auto destoyer( tag_t<some_type> ) {
    return val<destroy>;
  }
}

我们完成了。

factory::unique_ptr<some_ns::some_type>是将唯一的ptr存储到some_type的正确类型。要创建它,只需factory::make_unique<some_ns::some_type>( 7, 2 ),您将获得一个正确类型的唯一ptr,该ptr具有排成一行的销毁器,内存开销为零,并且对销毁器函数的调用不涉及任何间接操作。

基本上清除std::unique_ptrstd::make_unique的{​​{1}}和factory::unique_ptr,然后排列创建者/销毁者ADL助手以使所有唯一的ptrs成为给定类型,只需执行正确的事情。

测试代码:

factory::make_unique

live example

与编写自定义唯一ptr类型相比,此方法的运行时开销为零。如果您没有在auto ptr = factory::make_unique<some_ns::some_type>( 2, 3 ); std::cout << ptr->x << "=5\n"; std::cout << sizeof(ptr) << "=" << sizeof(std::unique_ptr<some_ns::some_type>) << "\n"; 的命名空间(或creator中)为destroyer重载tag_t<X>X,则namespace factory返回一个沼泽标准factory::make_unique。如果这样做,它将向编译时信息注入如何销毁它的信息。

默认情况下,std::unique_ptr<X>使用factory::make_unique<X>初始化,以便与聚合一起使用。

{}系统将启用基于tag_tfactory::make_unique的基于ADL的自定义。它在其他地方也很有用。最终用户不必了解它,他们只需要知道您始终使用factory::unique_ptrfactory::unique_ptr

搜索factory::make_uniquestd::make_unique应该会发现您有人违反此规则的情况。最终(您希望)他们会注意到所有唯一指针都是std::unique_ptr而不是factory::unique_ptr

向系统添加类型的魔力足够短且易于复制,人们无需了解ADL就可以做到。我会在某处的注释中添加一段标记为“您不需要知道这一点,但这就是它的工作原理”的段落。

这可能太过分了;我只是在考虑如何使用一些新功能来处理中的分布式特征和破坏/创建。我尚未在生产中尝试此操作,因此可能存在未公开的问题。

答案 1 :(得分:1)

您可以做的一件事是使用using语句为std::unique_ptr<T, void (*)(T*)>引入别名,例如

template<typename T>
using uptr = std::unique_ptr<T, void (*)(T*)>;

然后您可以像使用它

auto u = uptr<int>{create(), &destroy};

答案 2 :(得分:1)

我建议编写自定义删除器,而不要使用函数指针。使用函数指针会毫无疑问使所有unique_ptr的大小加倍。

相反,编写一个以函数指针为模板的删除器:

template <auto deleter_f>
struct Deleter {
    template <typename T>
    void operator()(T* ptr) const
    {
        deleter_f(ptr);
    }
};

或者,如Yakk - Adam Nevraumont mentioned in the comments

template <auto deleter_f>
using Deleter = std::integral_constant<std::decay_t<decltype(deleter_f)>, deleter_f>;

使用它变得很干净:

auto a = std::unique_ptr<int, Deleter<destroy>>{create()};

尽管您可能希望将其与make_unique_ptr函数结合使用:

template <auto deleter_f, typename T>
auto create_unique_ptr(T* ptr)
{
    return std::unique_ptr<T, Deleter<deleter_f>>{ptr};
}

// Usage:
auto a = create_unique_ptr<destroy>(create());

答案 3 :(得分:0)

您使事情变得过于复杂。 Simply specialize std::default_delete for your custom types,您可以使用香草std::unique_ptr

std::shared_ptr没有使用相同的自定义点,这很可惜,您将不得不显式提供删除程序。