如何使用 std::sort() 对指向值的向量进行排序?

Posted

技术标签:

【中文标题】如何使用 std::sort() 对指向值的向量进行排序?【英文标题】:How can can I sort a vector of pointed-to values using std::sort()? 【发布时间】:2019-04-09 13:44:35 【问题描述】:

已经有帖子sorting vector of pointers,但这不是关于指针向量,而是关于引用指针向量。

3 个整数被放入std::vector<int*>,然后根据指针后面的值进行排序。

#include <iostream>
#include <vector>
#include <algorithm>

int main() 
    int a = 3;
    int b = 2;
    int c = 1;

    std::vector<int*> vec;
    vec.emplace_back(&a);
    vec.emplace_back(&b);
    vec.emplace_back(&c);

    std::sort(vec.begin(), vec.end(), [](const int* a, const int* b) 
        return *a < *b;
    );

    std::cout << "vec = " << *vec[0] << ", " << *vec[1] << ", " << *vec[2] << '\n';
    std::cout << "abc = " << a << ", " << b << ", " << c << '\n';

但是,似乎只有向量被排序,如输出所示:

vec = 1, 2, 3 
abc = 3, 2, 1 

我认为原因是std::sort() 在正确比较时只是分配地址而不是值。这里有什么问题?为什么我不能对这个指向值的向量进行排序?

下一部分是 TL,DR,因为它展示了我解决此问题的方法。一项简单的任务,却发现自己相当令人沮丧地复杂。 @Bathsheba's answer 指出这是不可能的。所以接下来的部分,最初被认为是我尝试的介绍,现在可能被认为是为什么不可能的原因。


我的想法是制作一个指针类包装器来提供我自己的构造函数和赋值运算符。 std::sort() 如果容器的大小很小(在我的系统上为 &lt;= 32),则行为会有所不同,但在这两种情况下都会发生分配和移动——就像来自 _Insertion_sort_unchecked(来自 &lt;algorithm&gt;)函数的这个小 sn-p显示。

(_BidIt == std::vector&lt;int*&gt;::iterator_Iter_value_t&lt;_BidIt&gt; == int*)

_BidIt _Insertion_sort_unchecked(_BidIt _First, const _BidIt _Last, _Pr _Pred)
       // insertion sort [_First, _Last), using _Pred
    if (_First != _Last)
        
        for (_BidIt _Next = _First; ++_Next != _Last; )
               // order next element
            _BidIt _Next1 = _Next;
            _Iter_value_t<_BidIt> _Val = _STD move(*_Next);

            if (_DEBUG_LT_PRED(_Pred, _Val, *_First))
                   // found new earliest element, move to front
                _Move_backward_unchecked(_First, _Next, ++_Next1);
                *_First = _STD move(_Val);

让我们创建一个类assignement_pointer,它的行为类似于一个指针,只是它分配的是值而不是地址。

template<typename T>
class assignement_pointer 
public:
    assignement_pointer(T& value) 
        this->m_ptr = &value;
        std::cout << "<T>& constructor\n";
    
    assignement_pointer(const assignement_pointer& other) 
        this->m_ptr = other.m_ptr;
        std::cout << "copy constructor\n";
    
    assignement_pointer(assignement_pointer&& other) 
        std::cout << "move assignement constructor >> into >> ";
        *this = std::move(other);
    
    assignement_pointer& operator=(const assignement_pointer& other) 
        *this->m_ptr = *other.m_ptr;
        std::cout << "copy assignement operator\n";
        return *this;
    
    assignement_pointer& operator=(assignement_pointer&& other) 
        std::swap(this->m_ptr, other.m_ptr);
        std::cout << "move assignement operator\n";
        return *this;
    
    T& operator*() 
        return *this->m_ptr;
    
    const T& operator*() const 
        return *this->m_ptr;
    
private:
    T* m_ptr;
;

如您所见,还有临时的std::cout 用于查看主要通过std::sort() 时调用了哪些构造函数/赋值运算符:

    ///...
    std::vector<assignement_pointer<int>> vec;
    vec.reserve(3);
    vec.emplace_back(assignement_pointer(a));
    vec.emplace_back(assignement_pointer(b));
    vec.emplace_back(assignement_pointer(c));

    std::cout << "\nsort()\n";
    std::sort(vec.begin(), vec.end(), [](const assignement_pointer<int>& a, const assignement_pointer<int>& b) 
        return *a < *b;
    );

    std::cout << "\nvec = " << *vec[0] << ", " << *vec[1] << ", " << *vec[2] << '\n';
    std::cout << "abc = " << a << ", " << b << ", " << c << '\n';

给出输出:

<T>& constructor
move assignement constructor >> into >> move assignement operator
<T>& constructor
move assignement constructor >> into >> move assignement operator
<T>& constructor
move assignement constructor >> into >> move assignement operator

sort()
move assignement constructor >> into >> move assignement operator
move assignement operator
move assignement operator
move assignement constructor >> into >> move assignement operator
move assignement operator
move assignement operator
move assignement operator

vec = 1, 2, 3
abc = 3, 2, 1
std::sort() 只调用移动函数。 同样,vec 已排序,但 abc 未排序

最后一点是有道理的,因为只有移动函数被调用,所以永远不会调用复制赋值运算符assignement_pointer&amp; operator=(const assignement_pointer&amp; other);(它执行值赋值)。可以去掉不必要的拷贝构造函数和赋值运算符:

template<typename T>
class assignement_pointer 
public:
    assignement_pointer(T& value) 
        this->m_ptr = &value;
    
    assignement_pointer(const assignement_pointer& other) = delete;
    assignement_pointer& operator=(const assignement_pointer& other) = delete;
    assignement_pointer(assignement_pointer&& other) 
        std::cout << "move assignement constructor >> into >> ";
        *this = std::move(other);
    
    assignement_pointer& operator=(assignement_pointer&& other) 
        std::swap(this->m_ptr, other.m_ptr);
        std::cout << "move assignement operator\n";
        return *this;
    
    T& operator*() 
        return *this->m_ptr;
    
    const T& operator*() const 
        return *this->m_ptr;
    
private:
    T* m_ptr;
;

现在std::sort() 内部流程相当复杂,但最终归结为像std::swap() 这样的操作失败:

 int main() 
    int a = 3;
    int b = 2;

    std::vector<assignement_pointer<int>> vec;
    vec.reserve(2); //to avoid re-allocations
    vec.emplace_back(assignement_pointer(a));
    vec.emplace_back(assignement_pointer(b));

    std::cout << "swap()\n";

    assignement_pointer<int> ptr_a a ;
    assignement_pointer<int> ptr_b b ;

    std::swap(ptr_a, ptr_b);

    std::cout << "\nptrs = " << *ptr_a << ", " << *ptr_b << '\n';
    std::cout << "a, b = " << a << ", " << b << '\n';

正如此输出所示:

move assignement constructor >> into >> move assignement operator
move assignement constructor >> into >> move assignement operator
swap()
move assignement constructor >> into >> move assignement operator
move assignement operator
move assignement operator

ptrs = 2, 3
a, b = 3, 2

只是切换指针而不切换原始变量的位置。 std::swap基本上是

_Ty _Tmp = _STD move(_Left);
_Left = _STD move(_Right);
_Right = _STD move(_Tmp);

解释

move assignement constructor >> into >> move assignement operator
move assignement operator
move assignement operator

移动赋值运算符只是交换指针,因此创建临时变量不会做任何事情。 我看到了两种可能的解决方案:

使移动赋值运算符不是交换指针而是交换值。 为班级实现我自己的swap()

但两者都不起作用。

移动赋值运算符无法交换值,因为来自 this-&gt; 类的初始 m_ptr 始终是 nullptr,我不想取消引用它。 std::sort() 从不使用std::swap(),而只是使用来自各地的std::move()s。 (_Insertion_sort_unchecked 已经部分看到了)。

【问题讨论】:

这听起来像是一个 XY 问题。你为什么要让向量之外的变量发生变化?这是概念证明还是您真正需要它的情况? 问问自己:如果你撕下电话簿的页面并随机播放,这会改变任何人的电话号码吗? 您的第一个代码的行为正是人们所期望的,剩下的就是 tl;dr @NathanOliver 我想制作一个对参数进行排序的可变参数模板函数。我可以将它们复制到一个向量中,对向量进行排序,然后重新分配,但我想知道是否可以通过向量指针来做到这一点 一个更好的标题应该是“如何对指向的值进行排序?”。排序指针是另一回事(而且是你已经在做的事情)。 【参考方案1】:

您需要滚动您自己的排序功能来执行此操作。

回调 lambda 用于评估排序,但您需要调整执行实际元素交换的部分:C++ 标准库 sort 不支持您这样做。

幸运的是,没有任何花里胡哨(例如预随机化)的快速排序只需几十行,所以这不是一项特别繁重的任务。

【讨论】:

你是否总是保留你的答案时间戳以防止实际工作的人先得到它? @Slava:你知道吗?我花了 22 分钟才回复这个问题。 我希望有人会说“您需要滚动自己的排序功能才能做到这一点。”提供代码示例。有人可能已经在研究它,但你首先通过简单地说,你需要这样做,而不提供实际答案来得到你的答案。同样的方式你可以放在这里“我的答案稍后会在这里,我正在努力” @Slava:如果您认为这个答案没有用,请投反对票。我很清楚 OP 知道如何根据问题质量来判断代码,所以我的工作是指出方法。如果你用代码回答,我一定会投票。 @StackDanny:我想这是不可能的,因为没有人说服 C++ 标准委员会相信它可能有用。【参考方案2】:

只需保留原始指针向量的副本并将排序后的值复制过来:

            std::vector<int*> original = vec;  // Do this before the std::sort

然后在你打印 a,b,c:

            std::vector<int> xfer;
            for (auto ptr : vec) 
                xfer.push_back(*ptr);
            
            auto it = std::begin(xfer);
            for (auto ptr : original) 
                *ptr = *it++;
            
            std::cout << "abc = " << a << ", " << b << ", " << c << '\n';

输出:

abc = 1, 2, 3

【讨论】:

好吧,如果我这样做,我什至不需要指针,因为我可以复制 - 排序 - 复制回,这是我评论我宁愿不做的。但鉴于我最初的想法是不可能的,我要么这样做,要么自己做。因此,归根结底,Bathsheba 的答案与您的答案之间存在性能与可维护性的问题。

以上是关于如何使用 std::sort() 对指向值的向量进行排序?的主要内容,如果未能解决你的问题,请参考以下文章

使用 std::sort 对对象向量进行排序

C++ - 使用 std::sort 对结构向量进行排序导致读取访问冲突

使用 std::sort 对具有特定标准的二维向量进行排序

使用 std::sort() 对对象向量进行排序

使用 std::sort 对包含两个数据成员的对象向量进行排序

std::sort 对包含 Objects* 指针的 Objects 向量进行排序