unique智能指针在类中的赋值问题

https://www.fluentcpp.com/posts/ 非常好的讲解c++

https://wandbox.org/permlink/P9OKYwkxGqMnTGT2 在线c++执行

本文转自:https://www.fluentcpp.com/2018/11/06/how-to-transfer-unique_ptrs-from-a-set-to-another/

娓娓道来,当类中或者容器中有unique对象。为何复制时应该时move,总是被出错成:没有copy赋值函数。

How to Transfer unique_ptrs From a Set to Another Set

Published November 6, 2018 - 2 Comments

Daily C++

Transferring a std::unique_ptr to another std::unique_ptr is an easy thing to do:

std::unique_ptr<int> p1 = std::make_unique<int>(42);
std::unique_ptr<int> p2;

p2 = std::move(p1); // the contents of p1 have been transferred to p2

Easy peasy, lemon squeezy.

Now what if those unique_ptrs are living inside of two sets? It should be just as easy to transfer those in the first set over to the second set, right?

It turns out that it’s not easy, neither peasy, and even less lemon squeezy. Unless you have C++17, in which case it’s a breeze. But before C++17, it’s not. Here are various alternatives you can use to approach this.

Let’s see the motivating problem first.

move unique_ptr C++ sets

The case: transfering sets of unique_ptrs

We start by seeing what a std::set of std::unique_ptr would represent, and then we see what problem happens when trying to transfer the contents of one set to another.

Sets of unique_ptrs: unique and polymorphic

To begin with, you may have wondered why do a unique_ptr on an int as in the above example. Except for showing a simple example, well, it has no use at all.

A more realistic case would be one of runtime polymorphism via inheritance, with a Base class that can have Derived classes:

Base Derived

And we would use the base class polymorphically by holding it with some sort of handle (pointer or reference). To encapsulate the memory management, we would use a std::unique_ptr<Base>.

Now if we want a collection of several objects implementing Base, but that could be of any derived classes, we can use a collection of unique_ptr<Base>s.

Finally, we may want to prevent our collection to have duplicates. This is what std::set does. Note that to implement this constraint, std::set needs a way to compare its objects together.

Indeed, by declaring a set this way:

std::set<std::unique_ptr<Base>>

the comparison between elements of the set will call the operator< of std::unique_ptr, which compares the memory addresses of the pointers inside them.

In most cases, this is not what you want. When we think “no duplicates”, it generally means “no logical duplicates” as in: no two elements have the same value. And not “no two elements are located at the same address in memory”.

To implement no logical duplicates, we need to call the operator< on Base (provided that it exists, maybe using an id provided by Base for instance) to compare elements and determines whether they are duplicates. And to make the set use this operator, we need to customize the comparator of the set:

struct ComparePointee
{
    template<typename T>
    bool operator()(std::unique_ptr<T> const& up1, std::unique_ptr<T> const& up2)
    {
        return *up1 < *up2;
    }
};

std::set<std::unique_ptr<int>, ComparePointee> mySet;

To avoid writing this type every time we instantiate such a set in code, we can hide its technical aspects behind an alias:

template<typename T>
using UniquePointerSet = std::set<std::unique_ptr<T>, ComparePointee>;

Transferring unique_ptrs between two sets

Ok. We’re all set (ha-ha) and ready to transfer the elements of a set to another one. Here are our two sets:

UniquePointerSet<Base> source;
source.insert(std::make_unique<Derived>());

UniquePointerSet<Base> destination;

To transfer elements efficiently, we use the insert method:

destination.insert(begin(source), end(source));

But this leads to a compilation error!

error: use of deleted function 'std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = Base; _Dp = std::default_delete<Base>]'

Indeed, the insert methods attemps to make a copy of the unique_ptr elements.

What to do then?

C++17’s new method on set: merge

sets and maps in C++ are internally implemented as trees. This lets them ensure the algorithmic complexities guaranteed by the methods of their interface. Before C++17, it didn’t show in the interface.

C++17 adds the merge method to sets:

destination.merge(source);

This makes destination take over the nodes of the tree inside of source. It’s like performing a splicing on lists. So after executing this line, destination has the elements that source had, and source is empty.

And since it’s only the nodes that get modified, and not what’s inside them, the unique_ptrs don’t feel a thing. They are not even moved.

destination now has the unique_ptrs, end of story.

Now if you don’t have C++17 in production, which is the case of a lot of people at the time I’m writing these lines, what can you do?

We can’t move from a set

The standard algorithm to move elements from a collection to another collection is std::move. Here is how it works with std::vector:

std::vector<std::unique_ptr<Base>> source;
source.push_back(std::make_unique<Derived>());

std::vector<std::unique_ptr<Base>> destination;

std::move(begin(source), end(source), std::back_inserter(destination));

after the execution of this line, destination has the elements that source had and source is not empty, but has empty unique_ptrs.

Let’s try to do the same thing with our sets now:

UniquePointerSet<Base> source;
source.insert(std::make_unique<Derived>());

UniquePointerSet<Base> destination;

std::move(begin(source), end(source), std::inserter(destination, end(destination)));

We get the same compilation error as in the beginning, some unique_ptrs are getting copied:

error: use of deleted function 'std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&)

This may look surprising. The purpose of the std::move algorithm is to avoid making copies on the unique_ptr elements and move them instead, so why are they being copied??

关键就是set的iterator返回不是 unique_ptr&,而是const unique_ptr&。因为move要改变传入的对象内部状态,但由于有了const,所有编译器只能选择copy。

The answer lies in how the set provides access to its elements. When dereferenced, a set’s iterator does not return a unique_ptr&, but rather a const unique_ptr&. It is to make sure that the values inside of the set don’t get modified without the set being aware of it. Indeed, it could break its invariant of being sorted.

So here is what happens:

  • std::move dereferences the iterator on set and gets a const unique_ptr&,
  • it calls std::move on that references, thus getting a const unique_ptr&&,
  • it calls the insert method on the insert output iterator and passes it this const unique_ptr&&,
  • the insert method has two overloads: one that takes a const unique_ptr&, and one that takes a unique_ptr&&. Because of the const in the type we’re passing, the compiler cannot resolve this call to the second method, and calls the first one instead.

Then the insert output iterator calls calls the insert overload on the set that takes a const unique_ptr& and in turn calls the copy constructor of unique_ptr with that l-value reference, and that leads to the compilation error.

Making a sacrifice

So before C++17, moving elements from a set doesn’t seem to be possible. Something has to give: either moving, or the sets. This leads us to two possible aspects to give up on.

Keeping the set but paying up for the copies

第一种解决是赋值了对象,用cloneBase。

To give up on the move and accepting to copy the elements from a set to another, we need to make a copy of the contents pointed by the unique_ptrs.

For this, let’s assume that Base has is a polymorphic clone implemented by its method cloneBase, overriden in Derived:

class Base
{
public:
    virtual std::unique_ptr<Base> cloneBase() const = 0;

    // rest of Base...
};

class Derived : public Base
{
public:
    std::unique_ptr<Base> cloneBase() const override
    {
        return std::make_unique<Derived>(*this);
    }

    // rest of Derived...
};

At call site, we can make copies of the unique_ptrs from a set over to the other one, for instance this way:

auto clone = [](std::unique_ptr<Base> const& pointer){ return pointer->cloneBase(); };
std::transform(begin(source), end(source), std::inserter(destination, end(destination)), clone);

Or, with a for loop:

for (auto const& pointer : source)
{
    destination.insert(pointer->cloneBase());
}

Keeping the move and throwing away the set

第二种解决,vector不会用const,因此可以调用到move。执行完后,vector中的都清空了。全move到了set中。

The set that doesn’t let the move happen is the source set. If you only need the destination to have unique elements, you can replace the source set by a std::vector.

Indeed, std::vector does not add a const to the value returned by its iterator. We can therefore move its elements from it with the std::move algorithm:

std::vector<std::unique_ptr<Base>> source;
source.push_back(std::make_unique<Derived>(42));

std::set<std::unique_ptr<Base>> destination;

std::move(begin(source), end(source), std::inserter(destination, end(destination)));

Then the destination set contains a unique_ptr that has the contents that used to be in the one of the source, and the source vector now contains an empty unique_ptr.

Live at head

You can see that there are ways around the problem of transferring unique_ptrs from a set to another one. But the real solution is the merge method of std::set in C++17.

The standard library is getter better and better as the language evolves. Let’s do what we can to move (ha-ha) to the latest version of C++, and never look back.

Related articles:

对set的神奇补丁代码:

复制代码
 1 #include <iostream>
 2 #include <type_traits>
 3 #include <list>
 4 #include <vector>
 5 #include <string>
 6 #include <iterator>
 7 #include <set>
 8 #include <memory>
 9  
10  struct compare_ptr
11  {
12      template <typename ptr>
13      bool operator()(const ptr& lhs, const ptr& rhs) const
14      {
15          // make nullptr first in order
16          if (!lhs) return true;
17          if (!rhs) return false;
18          return *lhs < *rhs;
19      }
20  };
21  
22  template <typename T>
23  struct unique_ptr_front_pop_iterator
24  {  
25      unique_ptr_front_pop_iterator() : v(nullptr) {}
26      unique_ptr_front_pop_iterator(T& v) : v(&v) {}
27      
28      using value_type = typename T::value_type;
29      
30      value_type operator*() noexcept { 
31          const value_type& front = *(v->begin());
32          // it is ok to steal- since nullptr is always at the beginning and we just erase at once
33          value_type ret{const_cast<value_type&>(front).release()};
34          v->erase(v->begin());
35          if (v->empty())
36             v = nullptr; // job done
37          return ret;
38      }
39      
40      bool operator == (const unique_ptr_front_pop_iterator& other) const noexcept
41      {
42          // is job done? (see operator*)
43          return v == other.v;
44      }
45      bool operator != (const unique_ptr_front_pop_iterator& other) const noexcept
46      {
47          return !(*this == other);
48      }
49      
50      
51      
52      auto& operator ++() noexcept { return *this; } // input iterator only
53      
54      T* v;
55      
56      auto end() const
57      {
58          return unique_ptr_front_pop_iterator();
59      }
60      
61  };
62 
63 namespace std
64 {
65     template <typename T>
66     struct iterator_traits<unique_ptr_front_pop_iterator<T>>
67     {
68         using value_type = typename T::value_type;
69     };
70 }
71  
72  template <typename T>
73  auto make_unique_ptr_front_pop_iterator(T& v)
74  {
75      return unique_ptr_front_pop_iterator<T>(v);
76  }
77  
78 int main()
79 {
80     std::set<std::unique_ptr<std::string>,compare_ptr> s;
81     s.insert(std::make_unique<std::string>("one"));
82     s.insert(std::make_unique<std::string>("two"));
83     s.insert(std::make_unique<std::string>("three"));
84         
85     auto begin = make_unique_ptr_front_pop_iterator(s);
86     auto end = begin.end();
87  
88     std::set<std::unique_ptr<std::string>,compare_ptr> s2(begin, end);
89     
90     std::cout << "\ns2 now holds: ";
91     for (auto& str : s2)
92             std::cout << "\"" << *str << "\" ";
93     std::cout << "\noriginal list now holds: ";
94     for (auto& str : s)
95             std::cout << "\"" << *str << "\" ";
96     std::cout << '\n';
97 }
复制代码

 

posted @   Bigben  阅读(136)  评论(0编辑  收藏  举报
(评论功能已被禁用)
编辑推荐:
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
历史上的今天:
2017-11-03 Spring boot中使用aop详解
2017-11-03 装饰器模式
2016-11-03 JBoss 系列十四:JBoss7/WildFly如何加载外部的文件或properties文件
点击右上角即可分享
微信分享提示