【问题标题】:What will std::sort do if the comparison is inconsistent? (A<B, B<C, C<A)如果比较不一致,std::sort 会做什么? (A<B, B<C, C<A)
【发布时间】:2022-10-24 09:53:05
【问题描述】:

我需要按日期对文件列表进行排序。有this answer 怎么做。不过这让我很担心:它运行在一个可以在运行期间更改的实时文件系统上。

比较函数使用:

struct FileNameModificationDateComparator{
    //Returns true if and only if lhs < rhs
    bool operator() (const std::string& lhs, const std::string& rhs){
        struct stat attribLhs;
        struct stat attribRhs;  //File attribute structs
        stat( lhs.c_str(), &attribLhs);
        stat( rhs.c_str(), &attribRhs); //Get file stats                        
        return attribLhs.st_mtime < attribRhs.st_mtime; //Compare last modification dates
    }
};

据我了解,此函数可以并且将针对同一个文件多次调用,并将其与不同的文件进行比较。该文件可以在 sort 运行时被外部进程修改;一个较旧的文件可以在两次比较之间变成最新的,并且比一个相当旧的文件更旧,然后比一个最新的文件更新......

std::sort() 会做什么?我对结果中的一些稀少的订购错误很好。我对崩溃或冻结(无限循环)或其他此类不愉快感到不满意。我安全吗?

【问题讨论】:

  • 垃圾进垃圾出。您要么需要锁定系统,以便在此期间无法添加新文件,要么拍摄文件系统的快照并对其进行排序。您无法在数据集发生变异时对其进行排序。
  • 如果在排序过程中修改了基础数据(如文件),您将拥有未定义的行为将您需要的所有文件信息一次读入一个容器,然后引用这个固定的非修改容器进行排序。
  • 我已经看到它与一个糟糕的比较器崩溃了。
  • 即使忽略 UB,为了速度,我宁愿只 stat 文件一次。
  • 问题标题看起来像是在尝试对石头、论文和剪刀进行分类 :)

标签: c++ sorting


【解决方案1】:

我安全吗?

不。

std::sort 需要与严格弱序A&lt;B, B&lt;C, C&lt;A 违反了这一点。

这种违规会导致未定义的行为,并且在实践中会导致一些最糟糕的未定义行为。

还应该注意的是,任何为在排序期间任意更改排序的元素而编写的排序算法几乎是不可能的。算法永远不会知道整个集合当前已排序。

【讨论】:

  • 如果比较始终不一致( A<B, B<C, C<A总是) 它很可能会卡住。但是,如果它们只是像文件系统示例中那样偶尔进行实时变异,那么一个普通的旧 Bubblesort 将继续尝试,直到它获得一个良好的通过。
  • @SF .:但是列表不会在之后排序,因为文件系统在排序之后仍然可以改变。关键是,如果数据或标准发生变化,无论是否一致,没有算法可以保证排序。
  • @NicolBolas 该列表将具有来自突变的明显排序错误,但它会尽可能接近排序,并且如果花费比正常时间更多的时间,算法将很好地完成。
  • @SF。这里有有效点。从问题:“我对结果中的一些稀少的订购错误很好”
  • @SF。算法会很好地完成 - 也许不吧。问题是有调试运行时会检查您的排序标准是否确实遵循严格的弱顺序。例如,Visual C++ 调试运行时执行此操作 - 它通过调用您的排序条件函数两次来执行此操作,第一次与 a,b 进行比较,然后切换 b,a。如果存在不一致,则运行时断言()。如果检查完成后您的 a 和 b 文件信息发生了变化怎么办?
【解决方案2】:

std::sort() 假定集合是可排序的。

关系代数将一个集合定义为可排序的,如果:

  • 它是自反的,即a <= a 为真
  • 反对称,即:(a <= b and b <= a) <=> a = b
  • 传递性,即:(a <= b <= c) => a <= c

请参阅https://web.stanford.edu/class/archive/cs/cs103/cs103.1126/handouts/060%20Relations.pdf 第 7 页的偏序定义

在实践中,自反性不是必要的期望,因为即使 a < a 为假,但排序算法可能会不必要地交换相等的元素,因此强烈建议使其具有自反性。

您的问题陈述说您的集合上的关系不是传递的。但是请注意,它在任何时候都是严格传递的,问题是,在排序算法元素的(短)持续时间内可能会改变它们的值。

这不是一个定义明确的行为,在 C++ 中它是未定义的行为。

因此,我处理您的问题的方式是依靠它在任何时候都是可传递的这一事实。另外,为什么每次比较文件时都要测量文件大小?测量文件是 I/O 操作,会减慢您的进程。只测量一次文件更有意义,在对它们进行排序之前,将结果存储到一个集合中,其项目可能会改变它们的顺序,但值本身不会改变(文件1的大小将在算法之前测量,然后从那里开始,直到排序结束,您的集合中将保持不变,即使它不再为真)。

这种方法所涉及的风险是,结果会在测量后的几毫秒内被弃用,您已经指定为可以接受的问题。

此外,如果您经常需要这种排序,那么定期进行排序(可能每 10 分钟一次,或您选择的时间间隔一次)、缓存结果并在需要排序时参考缓存可能是有意义的。

【讨论】:

    【解决方案3】:

    正如其他答案已经说过的那样,交给std::sort一个不满足的比较器弱严格排序要求并在使用相同值多次调用时被保留将导致未定义的行为。

    这不仅意味着该范围最终可能无法正确排序,它实际上可能会导致更严重的问题,不仅在理论上,而且在实践中也是如此。一个常见的问题是您已经说过算法中的无限循环,但它也可能引入崩溃或漏洞。

    例如(我没有检查其他实现是否表现类似)我查看了 libstdc++ 的 std::sort 实现,它作为 introsort 的一部分使用插入排序。插入排序调用函数__unguarded_linear_insert,参见github mirror。此函数通过比较器对范围执行线性搜索,而不保护范围的结尾,因为调用者应该已经验证了搜索的项目将落入范围内。如果调用者中的保护比较和无保护线性搜索之间的比较结果发生变化,则迭代器将越界递增,这可能会产生堆溢出或空解引用或其他任何取决于迭代器类型的情况。

    演示见https://godbolt.org/z/8qajYEad7

    【讨论】:

      猜你喜欢
      • 2021-10-04
      • 1970-01-01
      • 1970-01-01
      • 2017-01-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-11-28
      • 2020-12-18
      相关资源
      最近更新 更多