qsort 在 C++ 中不适用于哪些类型?

Posted

技术标签:

【中文标题】qsort 在 C++ 中不适用于哪些类型?【英文标题】:What kinds of types does qsort not work for in C++? 【发布时间】:2011-09-04 17:17:58 【问题描述】:

std::sort 使用 std::swap 交换元素,而 std::swap 又使用复制构造函数和赋值运算符,保证在交换值时获得正确的语义。

qsort 通过简单地交换元素的底层位来交换元素,忽略与您正在交换的类型相关的任何语义。

即使qsort 不知道您要排序的类型的语义,它仍然非常适用于非平凡类型。如果我没记错的话,它适用于所有标准容器,尽管它们不是 POD 类型。

我认为qsort 在类型T 上正常工作的先决条件是T 是/普通可移动/。在我的脑海中,唯一不能轻易移动的类型是那些具有内部指针的类型。例如:

struct NotTriviallyMovable

    NotTriviallyMovable() : m_someElement(&m_array[5]) 

    int m_array[10];
    int* m_someElement;
;

如果您对NotTriviallyMovable 的数组进行排序,那么m_someElements 最终会指向错误的元素。

我的问题是:还有哪些类型不适用于qsort

【问题讨论】:

我发现这里使用标签move-semantics 有点混乱,因为它通常与C++0x 的移动语义相关。 基本上,在 C++ 中对任何非 POD 类型使用 qsort 是未定义的行为。从那时起,它会破坏的具体情况以及如何破坏并不那么重要:无论如何你都不应该使用它。 你为什么要首先使用qsort?在我检查过的所有平台上,std::sort 都更快(对于可轻松交换的对象类型),而且考虑到内联比较运算符的选项,这是完全合理的。 我最后一次测量std::sort 的速度大约是原来的两倍,因为它就地排序(没有内存分配)。使用 C++0x,我们甚至可以免费获得大多数类型的移动构造函数,因此交换变得与位复制一样好……在安全的情况下。那么,您为什么还要打扰 qsort 呢? @Christopher @Matthieu:一个很好的原因是代码膨胀。上次我检查时,每个独特的用法会增加大约 5KB 的代码。在不在热路径上的代码中,最好使用更小的代码而不是更快的代码。此外,即使在热路径中,最好使用 qsort 以便更好地使用 I-cache。 【参考方案1】:

任何不是 POD 类型的类型都不能与 qsort() 一起使用。如果您考虑 C++0x,则可能有更多类型可用于 qsort(),因为它会更改 POD 的定义。如果您打算在 qsort() 中使用非 POD 类型,那么您就处于 UB 领域,守护程序将从您的鼻子飞出。

【讨论】:

这不是真的。例如std::vector 只是有一个指向外部数据的指针,交换底层字节将保持其所有不变量。诚然,这有点“hacky”,但如果我没记错的话,它会起作用。 @Peter:你完全错了。这样做不仅仅是“hacky”或使用特定于实现的行为——它完全是未定义的。 @DeadMG:标准所说的未定义内容与编译器在实践中所做的内容之间存在很大差异。例如,极受欢迎的 Fast Delegates 库 (codeproject.com/KB/cpp/FastDelegate.aspx) 充斥着技术上未定义的行为,但仍然在使用(并且有效)。 @Peter Alexander:不同之处在于这些实现不能随心所欲地改变行为。他依赖于实施的各个方面,这些方面在实践中是明确定义的,如果不是在文字上的话。另一方面,你只是在祈祷,任何实现都可能改变,嗯,几乎是它自身的任何方面,并且可怕地破坏你正在做的事情并保持一致。 即使是像std::string 这样“简单”的类型,当您使用诸如小字符串优化之类的智能实现时也会变得不平凡。那些很可能指向std::string 本身。【参考方案2】:

你完全错了。任何与qsort 一起工作的非 POD 类型都是完整的,完全幸运。如果您将处女的血献给众神并先跳一段小舞,那么它恰好在您的平台上与您的编译器一起在蓝月亮上为您工作并不意味着它确实有效。

哦,这是另一个用于非平凡可移动类型的类型,其实例是从外部观察到的。你移动它,但你没有通知观察者,因为你从未调用过交换或复制构造函数。

【讨论】:

对于外部观察者,即使您使用std::sort 也不起作用(除非对象被排序/知道/关于其外部观察者)。 @Peter:暗示在您类型的复制/交换功能中,您会通知他们。【参考方案3】:

“如果我没记错的话,它适用于所有标准容器”

整个问题归结为,在什么实施中?您是想按照标准编写代码,还是按照今天摆在您面前的编译器的实现细节编写代码?如果是后者,那么如果你所有的测试都通过了,我想它可以工作。

如果您询问的是 C++ 编程语言,则需要 qsort 才能仅用于 POD 类型。如果您要询问具体的实现,是哪一个?如果你问的是所有的实现,那么你就错过了机会,因为这种投票表决的最佳地点是 C++0x 工作组会议,因为他们聚集了几乎每个组织的代表积极维护的 C++ 实现。

对于它的价值,我可以很容易地想象std::list 的实现,其中列表节点嵌入到列表对象本身中,并用作头/尾哨兵。我不知道什么实现(如果有的话)实际上是这样做的,因为使用空指针作为头/尾标记也很常见,但肯定有一些优点实现一个每个都有一个虚拟节点的双向链表结尾。这样一个std::list 的实例当然不能轻易移动,因为它的第一个和最后一个元素的节点将不再指向哨兵。它的 swap 实现和(在 C++0x 中)它的移动构造函数将通过更新那些第一个和最后一个节点来解决这个问题。

没有什么可以阻止您的编译器在其下一个版本中切换到 std::list 的此实现,尽管这会破坏二进制兼容性,因此考虑到大多数编译器的管理方式,它必须是一个主要版本。

类似地,map/set/multimap/multiset 四重奏可以有指向其父节点的节点。任何容器的调试迭代器都可能包含指向容器的指针。为了做你想做的事,你必须(至少)在其实现的任何部分排除指向容器的任何指针的存在,并且像“没有实现使用任何这些技巧”这样的笼统声明是非常不明智的。制定标准的全部意义在于对所有符合标准的实现做出陈述,因此,如果您还没有从标准中推断出您的结论,那么即使您的陈述今天是正确的,明天也可能变得不正确。

【讨论】:

作为记录,我有一个列表实现,它的工作原理与史蒂夫的描述完全一样。链表的“链接”部分与数据分离,list_nodelist_head(没有数据成员)都继承自链接部分。 list_head 嵌入在列表对象本身中以保存动态分配。 list_head 的交换函数和移动构造函数调整第一个和最后一个列表元素的指针,使其跟随list_head 的移动。这不适用于 qsort! 我实际上并不关心标准容器,我只是将它们用作可能通过交换字节来交换的非 POD 类型的示例。我的问题是问它不适用于哪些类型,而不是它是否适用于标准容器。 您的问题是:“还有哪些类型不起作用?”。我的回答是,“除其他外,标准容器”。【参考方案4】:

这对于具有指向“相关”对象的指针的类型也不起作用。这样的指针有很多与“内部”指针相关的问题,但是要准确地证明什么是“相关”对象要困难得多。

一种特定的“相关”对象是带有反向指针的对象。如果对象 A 和 B 进行了位交换,并且 A 和 C 相互指向,那么之后 B 将指向 C,而 C 将指向 A。

【讨论】:

以上是关于qsort 在 C++ 中不适用于哪些类型?的主要内容,如果未能解决你的问题,请参考以下文章

使用 qsort 对 long long int 数组进行排序不适用于大型 nos

c++中参考C语言的qsort函数实现一个一个能对任意数据类型(包括结构体)的数组进行排序的函数

SWIG 多参数类型映射适用于函数,但如果有多个构造函数,则不适用于构造函数

冒泡排序不适用于对 C++ 中的动态对象数组进行排序

qsort函数排序各种类型的数据。

“AbstractButton 类型中的方法 addActionListener(ActionListener) 不适用于参数”错误