【问题标题】:How to properly return a collection of unique_ptr如何正确返回 unique_ptr 的集合
【发布时间】:2023-03-17 03:20:01
【问题描述】:

在更改我的代码以使用唯一指针后,我偶然发现了如何将对象集合返回给客户端。一般来说,我想将对象作为引用或非拥有指针传递。但如果我有一个对象集合,我不能只返回对它的引用。

例如,我有一个包含对象集合的简单类,这些对象都创建一次,之后不会更改。

using ObjectUPtr = std::unique_ptr<Object>;
class MyClass
{
  public:
  const std::vector<Object*>& GetObjectsOldStyle() const
  {
    return mObjectsOldStyle;
  }

  const std::vector<VObjectUPtr>& GetObjectsNewStyleA() const
  {
    // I don't like that: The client should not see the unique_ptr ...
    return mObjectsNewStyle; 
  }

  std::vector<VObject*> GetObjectsNewStyleB() const
  {
    // Ok, but performance drops
    std::transform(...); // Transform the collection and return a copy
  }

  const std::vector<VObject*>& GetObjectsNewStyleC() const
  {
    // Ok, only copied once, but two variables per collection needed
    // Transform the collection and cache in a second vector<Object*>
    std::transform(...);
  }

  std::vector<Object*> mObjectsOldStyle;    // old-style owning pointers here
  std::vector<ObjectUPtr> mObjectsNewStyle; // how I want to do it today
}

今天,我通常更喜欢 GetObjectsNewStyleB,但我想知道是否有更优雅和有效的方式或一般的最佳实践来返回此类集合。

【问题讨论】:

    标签: c++ c++11 vector unique-ptr


    【解决方案1】:

    我建议创建您自己的迭代器类。然后创建开始和结束成员函数。您甚至可以重载取消引用运算符以返回引用,而不是指针(除非您的指针可能为空)。它可能会像这样开始:

    class iterator :
        public std::iterator<std::random_access_iterator_tag, Object>
    {
    public:
        Object& operator*() const { return **base; }
        Object* operator->() const { return &**base; }
        iterator& operator++() { ++base; return *this; }
    
        // several other members necessary for random access iterators
    private:
        std::vector<ObjectUPtr>::iterator base;
    };
    

    实现符合标准的迭代器有点乏味,但我认为这是迄今为止最惯用的解决方案。如 cmets 中所述,Boost.Iterator 库,特别是 boost::iterator_facade 可用于减轻一些乏味。

    【讨论】:

    • 另外,使用boost.iterator 来实现这一点可以避免一些乏味。
    • 如果我没听错的话,这意味着为 stl 容器实现一种包装器(例如,如下所述:accu.org/index.php/journals/1527)。我试过了,很高兴以 for (auto obj : myClass.Objects) { ... } 之类的方式使用它,但这意味着我需要为模型中的所有集合编写一个包装器。我已经这样做了,但不知何故我希望有一种更简单的方法,可以将任何类型的 stl 容器直接转换为具有 unique_ptr 的原始指针。
    • 我尝试了您的解决方案:效果很好,API 正是我想要的。还有一点很棘手:如果我将对象存储在容器中,但我想在 API 上返回这些对象的纯接口:在这种情况下,我不能使用迭代器,因为我需要有关底层对象的知识类型。我可以通过使用与“拥有”容器并行的第二个容器来解决这个问题,但这似乎有点奇怪。下面的 Adam 的接收器解决方案在这里更加灵活,但也不是我所期望的通过简单的 API 访问集合。
    • @badstoms:对不起,我不明白您所说的“在 API 上返回这些对象的纯接口”是什么意思。有点模糊。
    • @Benjamin:我的意思是次要问题,与这个问题无关:VObject 派生自 IObject(纯接口)您存储了 VObject 的集合,但您想返回 IObject 的集合(作为纯接口)到客户端。我想,转换/复制是不可避免的。你不能弯曲迭代器,因为它们是被复制的。
    【解决方案2】:

    您不必返回集合,从而破坏了容器类的封装。还有其他选择。

    我会选择 Enumerator/Receiver 模式(我不知道这是否是该模式的实际名称)。

    基本思想是让 API 的客户端实现一个接口,该接口原则上从容器中一个接一个地接收对象。

    看起来像这样:

    class Receiver {
      public:
        virtual void receive(const Object& object) = 0;
    };
    
    class Container {
      public:
        void enumerate(Receiver& receiver) const {
          for (auto&& obj : m_objects) {
            receiver.receive(*obj);
          }
        }
    
      private:
        std::vector<ObjectUPtr> m_objects;
    };
    

    然后实现Receiver接口:

    class ReceiverImpl : public Receiver {
      public:
        virtual void receive(const Object& object) {
          // do something with object
        }  
    };
    

    并让容器将对象枚举给接收者:

    Container container;
    ReceiverImpl receiver;
    container.enumerate(receiver);
    

    live example

    此外,您甚至可以通过在Container::enumerate 中添加互斥锁/解锁来使容器线程安全,而客户端甚至不会注意到!

    最后,您可以将Container::enumerate 中的接收器参数替换为模板参数,以消除虚函数调用的运行时开销。

    【讨论】:

    • 这会使随机访问元素变得更加复杂。但我现在明白(并且同意你的观点),直接暴露 stl 容器是一个坏主意,原因有很多。
    • 对,但仍然...您可以将Container::enumerate(std::size_t idx, Receiver&amp; receiver) 重载添加到容器类中,以提供随机访问的便利。一般来说,这个问题是关于封装和可用性之间的权衡。
    【解决方案3】:

    如果在这种情况下使用 boost,我更喜欢 tranform_iterator 而不是 iterator_facade

    为了获得更实用的风格,我将代码分成两部分:

    • 创建一个可以将两个指针作为一个范围保存的类(允许基于范围的 for 循环)。
    • 创建一个函数,将 lambda 作为转换步骤并返回一个范围(以隐藏 transform_iterator)。

    因此,在取消引用迭代器时执行转换步骤。

    代码示例

    #include <boost/iterator/transform_iterator.hpp>
    #include <memory>
    #include <vector>
    #include <algorithm>
    
    #include <stdio.h>
    
    using namespace boost;
    using namespace std;
    
    template <typename Iterator>
    class Range {
        public:
            Range(Iterator begin, Iterator end) : b(begin), e(end) { }
            Range(const Range &r) = default;
            Range(Range &&r) = default;
            Range &operator=(const Range &r) = default;
            Range &operator=(Range &&r) = default;
    
            template <typename Container>
                Range(Container &c) : b(c.begin()), e(c.end()) { }
    
            Iterator begin() { return b;}
            Iterator begin() const { return b; }
    
            Iterator end() { return e;}
            Iterator end() const { return e; }
    
            Iterator b;
            Iterator e;
    };
    
    
    template <typename Container, typename TransformFunc>
    Range<transform_iterator<TransformFunc, typename Container::iterator>>
    transform(Container &c, TransformFunc f) {
        using namespace boost;
        using cont_it = typename Container::iterator;
        using iterator = transform_iterator<TransformFunc, cont_it>;
    
        iterator b = iterator(c.begin(), f), e = iterator(c.end(), f);
        Range<iterator> r(b,e);
    
        return r;
    }
    
    int main(int, char **) {
        vector<unique_ptr<int>> foo;
    
        for (int i = 0; i < 10; i++) {
            foo.push_back(unique_ptr<int>(new int(10)));
        }
    
        auto f = [](unique_ptr<int> &i) { return i.get(); };
        for (auto *i : transform(foo, f) ) {
            printf("%p ", i);
        }
        return 0;
    }
    

    【讨论】:

      猜你喜欢
      • 2016-07-22
      • 1970-01-01
      • 2019-11-13
      • 2011-07-09
      • 2014-10-25
      • 1970-01-01
      • 2020-09-29
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多