【问题标题】:how to use lambda with templated std::unique_ptr?如何将 lambda 与模板化 std::unique_ptr 一起使用?
【发布时间】:2021-05-02 11:01:02
【问题描述】:

nvinfer1::IRuntimenvinfer1::ICudaEngine 等NVIDIA TensorRT 对象不能直接存储在std::unique_ptr<> 中。相反,他们有一个必须调用的destroy() 方法。

因此,要使其工作,您必须使用这样的删除器:

#include <NvInfer.h>
#include <cuda.h>

template<typename T>
struct NVIDIADestroyer
{
    void operator()(T * t)
    {
        t->destroy();
    }
};

template<typename T>
using NVIDIAUniquePtr = std::unique_ptr<T, NVIDIADestroyer<T>>;

然后你使用NVIDIAUniquePtr&lt;T&gt;,而不是std::unique_ptr&lt;T&gt;

到目前为止,这工作正常。然后我在清理代码时尝试做的是用 lambda 替换删除器,这样我就可以跳过定义 NVIDIADestroyer 结构。但我无法弄清楚如何做到这一点。我的想法是这样的:

template<typename T>
using NVIDIAUniquePtr = std::unique_ptr<T, [](T * t)
{
    t->destroy();
}>;

但这会导致以下错误消息:

TRT.hpp:52:45: error: lambda-expression in template-argument
using NVIDIAUniquePtr = std::unique_ptr<T, [](T * t)
                                             ^
TRT.hpp:55:2: error: template argument 2 is invalid
  }>;
  ^

有没有办法让它工作?

【问题讨论】:

  • 这是由this question 回答的,虽然它不是重复的(我确定有一个,只是现在找不到)。
  • 为什么你认为 lambda 方法会更好?自定义删除器在一个地方解决了问题,您完成了,使用 lambda 的版本每次创建 std::unique_ptr 时都必须处理自定义删除器。
  • 在 C++20 中你可以拥有template &lt;typename T&gt; using NVIDIADestroyer = decltype([](T* t){ t-&gt;destroy(); });
  • C++ 一直在为此使用自定义类型。 (以前有比较器)在 C++20 之前,您可以使用函数指针作为类型,并在指针中传递对象(除了 unique_ptr 不允许,仅用于比较器对象)但使用类型使优化器更容易内联代码。在 C++20 中,lambda 只是用于定义结构的语法糖。 tl;dr:您已经拥有的代码是惯用的 C++
  • @MichaëlRoy 不必要的开销。

标签: c++ lambda c++17 unique-ptr tensorrt


【解决方案1】:

自 C++11 以来,使用定义为 structclass 的无状态删除器的运行时间和空间开销为零,再好不过了。

使用函数模板代替删除器的类模板,无需指定删除器类模板参数,也不必包含 CUDA 头文件。

noexcept 在删除函数上可能会导致更小的调用代码。因为在 noexcept 调用周围的调用者中不需要编译器生成的堆栈展开代码。 (GNU C++ 标准库~unique_ptr()noexcept 无条件,但C++ 标准并不要求这样做。GNU C++ 标准库可能正是出于我所说的原因为您做到这一点。太糟糕了noexcept 没有被推断和应用由编译器自动执行,出于 ABI 稳定性的原因(#1 原因我们在 C++ 中不能有好的东西),理论上可以用明确的用户提供的noexcept 规范覆盖,但这本身就是一个大主题。)

从 C++17 开始,无捕获的 lambda 闭包也可以用作零开销的删除器:

#include <memory>
#include <iostream>

// C++11
struct Deleter { template<class P> void operator()(P p) noexcept { p->destroy(); } };
template<class T> using P11 = std::unique_ptr<T, Deleter>;

// C++17
constexpr auto deleter = [](auto p) noexcept { p->destroy(); };
template<class T> using P17 = std::unique_ptr<T, decltype(deleter)>;

int main() {
    std::cout << sizeof(void*) << '\n';
    std::cout << sizeof(P11<void>) << '\n';
    std::cout << sizeof(P17<void>) << '\n';
}

使用-std=c++17 编译输出:

8
8
8

【讨论】:

    【解决方案2】:

    另一种方法是包装您的怪异删除对象以使其行为。也许是这样的:

    template <typename Wrapped>
    struct raiified : Wrapped {
    
        template <typename... Args>
        raiified(Args&&... args) : Wrapped(std::forward<Args>(args)...) { }
    
        raiified(const Wrapped& wrapped) : Wrapped(wrapped) { }
        raiified(Wrapped&& wrapped) : Wrapped(wrapped) { }
        raiified(const raiified& other) : wrapped(other.wrapped) { }
        raiified( raiifiedd&& other) : Wrapped(std::move(other.wrapped)) { }
    
        // operator overloads?
    
        ~raiified() { wrapped.delete(); }
    }
    

    这样,您应该可以使用std::unique_ptr&lt;raiified&lt;nvinfer1::IRuntime&gt;&gt;。或者也许:

    namespace infer {
    
    template <typename T>
    using unique_ptr = std::unique_ptr<raiified<T>>
    
    }
    

    【讨论】:

      【解决方案3】:

      lambda 函数(闭包)很难按特定的 TYPE 存储,例如 std::function,但我们可以通过手动内存管理将其存储在 heap-zone 中。

      请注意,C++ 标准不保证 lambda 函数可轻松复制,因此我们无法直接对其进行 memcpy,而只能对其进行包装。

      参考:asio/experimental/detail/channel_service.hpp:try_receive

      请看下面的伪代码:

      
      class MyWrapper {
      
      MyWrapper(MyWrapper&) = delete;
      
      void *mem;
      
      template<typename T>
      void set(T&& token) { 
          mem = malloc(sizeof(token));
          new (mem) T(std::move(token));
      }
      
      template<typename T>
      void release() {
          ((T*)mem)->~T();
          free(mem);
      }
      
      }; //end class
      

      在构造/解构这个结构之前我们必须知道类型 T,而且我们不能避免它,因为没有运行时类型支持 C++。但是,我们可以在没有typename T 的情况下调用lambda,方法是将执行程序绑定到这个结构中。 (比如 std::bind,或者只是 lambda 函数的包装)

      【讨论】:

        【解决方案4】:

        这是使用 lambda 函数作为删除器的正确语法,这是您最初的问题。

        #include <functional>
        #include <memory>
        
        template <typename T>
        using NVIDIAUniquePtr = std::unique_ptr<T, std::function<void(T*)>>;
        
        template <typename T, typename... Args>
        NVIDIAUniquePtr<T> make_nvidia_unique(Args&&... args) {
          return NVIDIAUniquePtr<T>(
              new T(std::forward<Args>(args)...), [](T* p) {
                p->destroy();
                delete p;
              });
        }
        

        但是,正如许多人所指出的,这会增加 unique_ptr 的大小。这对您来说可能重要,也可能不重要,但有更好的方法......

        使用大小为 0 的删除器对象将为每个对象节省 32 个字节。删除器类中的模板化移动构造函数对于将 unique_ptr&lt;T&gt; 移动到 T 的基类的 unique_ptr 中是绝对需要的,这意味着任何生产代码都需要它们。

        Maxim Egorushkin 的解决方案很好,但并不完整。

        #include <iostream>
        #include <memory>
        #include <type_traits>
        
        template <typename T>
        struct nv_deleter {
          nv_deleter() noexcept = default;
          nv_deleter(nv_deleter&&) noexcept = default;
        
          template <typename U,
                    typename = std::enable_if_t<std::is_convertible<U*, T*>::value>>
          nv_deleter(nv_deleter<U>&&) noexcept {}
        
          template <typename U,
                    typename = std::enable_if_t<std::is_convertible<U*, T*>::value>>
          nv_deleter& operator=(nv_deleter<U>&&) noexcept {}
        
          // It is important that this does not throw.  If destroy() may throw, 
          // add a try/catch block.  
          void operator()(T* p) noexcept { 
            p->destroy();
            delete p;    // this is needed, if destroy() does not call delete, 
                         // you will end up with memory leaks 
          }
        };
        
        template <typename _T>
        using NVIDIAUniquePtr = std::unique_ptr<_T, nv_deleter<_T>>;
        
        template <typename T, typename... Args>
        auto make_nvidia_unique(Args&&... args) {
          return NVIDIAUniquePtr<T>(new T(std::forward<Args>(args)...),
                                    nv_deleter<T>{});
        }
        
        int main() {
          struct Base {
            virtual ~Base() noexcept {}
            virtual void destroy() noexcept {}  // it is preferable that this is noexcept,
                                                // but you may not have any control 
                                                // over external library code 
          };
        
          struct Derived : Base {};
        
          NVIDIAUniquePtr<Derived> pDerived = make_nvidia_unique<Derived>();
        
          // this is the case where the templated move constructors are necessary. 
          NVIDIAUniquePtr<Base> pBase = std::move(pDerived);
        
          std::cout << sizeof(pDerived) << '\n';  // prints sizeof(void*) -> 8
          std::cout << sizeof(pBase) << '\n';     // prints sizeof(void*) -> 8
        
          return 0;
        }
        

        【讨论】:

        • 不幸的是,这会带来不小的成本。 OPs 类型有一个大小为零的删除器,因此它们的 unique_ptr 的总大小是指针的大小。使用std::function 会向它添加另外32 个字节(或您的实现想要的内联存储)以进行复制。它“太强大了”——我们只会调用一个函数,但 std::function 可以调用任何东西。
        • 好吧,OP 似乎并不关心这 32 个字节......当使用 lambda 时它们总是存在的。另一种方法是克隆 std::default_deleter,但我该关闭笔记本电脑并吃晚饭了 :)。无论如何,我确信封装的对象 nvinfer1::IRuntime 和 nvinfer1::ICudaEngine 本身都比 32 字节大得多。
        • lambda 不需要占用空间。专业化 default_deleter 绝不是一个好主意。
        • 我不建议专门化 default_deleter。我建议克隆它。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-08-29
        • 2017-05-22
        • 2015-05-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多