使用 directory_iterator 时文件名 c_str() 损坏

Filename c_str() corruption when using directory_iterator

在使用 directory_iterator 存储文件名 c_str() 的目录中遍历所有文件时,会导致无效读取(和垃圾输出)。

我觉得这很奇怪。

代码示例:

工作:

#include <iostream>
#include <filesystem>

namespace fs = std::filesystem;

int main() {
 for (auto const &entry : fs::directory_iterator("./")) {
   std::cout << entry.path().filename().c_str() << '\n';
 }
}

valgrind reports no errors.

损坏的输出:

#include <iostream>
#include <filesystem>

namespace fs = std::filesystem;

int main() {
 for (auto const &entry : fs::directory_iterator("./")) {
   auto filename = entry.path().filename().c_str();
   std::cout << filename << '\n';
 }
}

valgrind reports 159 invalid reads (of size 1) -- the exact number depends on how many files are in the directory.

这两个片段都是使用 gcc 9.1 使用以下命令编译的: g++-9.1 test.cpp -std=c++17

临时对象的生命周期仅限于创建它的语句。通俗地说,语句就是以分号结尾的一行代码。所有临时对象都保持活动状态,直到整个语句结束。

From the C++ spec:

When an implementation introduces a temporary object of a class that has a non-trivial constructor ([class.default.ctor], [class.copy.ctor]), it shall ensure that a constructor is called for the temporary object. Similarly, the destructor shall be called for a temporary with a non-trivial destructor ([class.dtor]). Temporary objects are destroyed as the last step in evaluating the full-expression ([intro.execution]) that (lexically) contains the point where they were created. This is true even if that evaluation ends in throwing an exception. The value computations and side effects of destroying a temporary object are associated only with the full-expression, not with any specific subexpression.

剖析工作示例,我们看到 operator<< 在销毁临时对象之前执行。

  • entry.path() = 临时 #1
  • .filename() = 临时 #2
  • .c_str() 从临时#2
  • 中获取字符指针
  • .c_str() 传递给 std::coutoperator<<,而以上所有内容仍然存在
  • 调用 operator<< 获取 .c_str() 指针并执行 returns。
  • operator<< 的调用执行 '\n' 并且 returns.
  • 所有的临时文件都被销毁了。

剖析损坏的示例,我们看到一个悬空指针:

  • entry.path() = 临时 #1
  • .filename() = 临时#2
  • .c_str() 从临时#2 中获取字符指针并存储在变量 filename
  • End-of-statement: 所有的临时物都被破坏了。现在 filename 指向已删除的内存——它是一个悬挂指针。
  • operator<< 的调用传递了一个悬空指针,它取消引用,就好像它是一个有效的字符串 = 未定义的行为。

您可以通过删除 .c_str() 来提取局部变量而不会损坏,这会使变量 filename 成为类型 std::filesystem::path 的对象。 std::filesystem::path 拥有它的内存(类似于 std::string)。

for (auto const &entry : fs::directory_iterator("./")) {
    auto filename = entry.path().filename();
    std::cout << filename << '\n';
}

path also supports ostream output directly,不需要 .c_str().