4

C++ 17 的 filesystem 和文件时间

 3 years ago
source link: https://blog.csdn.net/orbit/article/details/114648940
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

C++ 17 的 filesystem 和文件时间

本文简单介绍了 C++ 17 的 filesystem,对其文件时间处理部分的不完整性也做了分析。

std::filesystem

在 C++ 17 标准之前,C++ 程序员在操作文件的时候,只能沿用 C 语言提供的方法。C 语言只提供了最基本的操作,随便做一个遍历磁盘目录的操作,就要用 findfirst,findnext 搞半天。尤其的对文件名的处理,不得不在 string 和 char * 之间来回倒腾。路径要自己分割和拼接,即便是在 string 提供了 “+” 和 “+=” 操作符的重载便利,那到底是用 ‘/’ 还是用 ‘\\’ 依然要抉择一下,很多开源的库中你会看到这样的代码:

  #ifdef WIN32
      fullpath = parentpath + "\\" + filename; 
  #else
      fullpath = parentpath + "/" + filename; 
  #endif

这种“苦日子”终于看到了一点曙光,C++ 17 终于提供了文件系统:std::filesystem。std::filesystem 应该算是 C++ 1X 以来新增内容中最简单的一块内容了,常用的内容总体上可分为四部分,分别是 path 对象、directory_entry 对象、directory_iterator 对象和一组文件操作方法,比如删除文件,复制文件等等。

path 对象

path 对象不对应于操作系统上任何目录或文件对象,它就是一个位置描述符,类似于 HTTP 协议中的 URL。但是不要小看它,有了它,操作文件名再也不用 split 来,strcat 去了,一个 path 对象就搞得清清楚楚。

path::parent_path() 得到父路径

path::filename() 得到文件名(带扩展名,如果不要扩展名,可用 path::stem() )

path::extension() 得到扩展名。

最重要的是,再也不用犹豫不决用 ‘/’ 还是用 ‘\\’ 了,只要用 path 对象直接 ‘+’ 就行了,无论你写成:

  stdfs::path pathname("C:\\Windows")
  stdfs::path pathname("C:\\Windows\\")

拼接目录以后:

  pathname += "system32";

得到的都是:

  "C:\Windows\system32"

可以用 is_absolute() 和 is_relative() 方法判断一个 path 是相对路径还是绝对路径,当然,也可以用 lexically_relative() 方法计算两个目录之间的相对路径:

   assert(stdfs::path("/a/d").lexically_relative("/a/b/c") == "../../d");

directory_entry 对象

不要顾名思义,这个 directory_entry 对象不仅仅代表目录,它还可以存储文件的信息。可以将其理解为是一个操作系统中文件实体信息的存储器,遍历目录的时候,每个directory_iterator 对象中存储的就是遍历得到的 directory_entry 对象列表。

可以用 is_directory() 方法 判断一个 directory_entry 对象是否是一个目录,可以用 last_write_time() 得到目录或文件的最后修改时间,当然,这个时间目前换算成系统时间或本地时间还有点麻烦,具体本文后面会介绍。如果 directory_entry 对象是个文件,还可以用 file_size() 获取文件的大小。

directory_iterator 对象

directory_iterator 其实不是一个迭代器,它只是一个 directory_entry 对象的容器,一般用于遍历目录和文件。directory_iterator 只遍历当前一级目录,如果要递归遍历整个目录树,需要用 recursive_directory_iterator。有了 directory_iterator ,遍历目录这样的操作只要一行代码就搞定了:

  stdfs::path base_folder("C:\\Windows\\system32");
  stdfs::directory_iterator ffitems(base_folder);
  for (auto& fe : ffitems)
  {
      stdfs::path new_path = base_folder;
      std::string new_name = (boost::format("%05d\n") % index).str();
      new_path += new_name;
      stdfs::rename(fe.path(), new_path);  //rename 文件和目录
  }

C++ 程序员可以把注意力都放在对遍历结果的处理上,再也不用 findfirst、findnext 造了半天轮子,把正事儿耽误了。

文件和目录操作

尽管 directory_entry 对象提供了 file_size(),exist() 方法获取一些 directory_entry 实例对应的磁盘文件的信息,但是更多的操作方法都不在 directory_entry 对象实现,而是直接由 std::filesystem 提供相应的操作,具体有:

  filesystem::absolute  
  filesystem::canonical  
  filesystem::weakly_canonical   
  filesystem::relative   
  filesystem::proximate   
  filesystem::copy   
  filesystem::copy_file    
  filesystem::copy_symlink  
  filesystem::create_directory
  filesystem::create_directories  
  filesystem::create_hard_link  
  filesystem::create_symlink  
  filesystem::create_directory_symlink  
  filesystem::current_path  
  filesystem::exists  
  filesystem::equivalent  
  filesystem::file_size  
  filesystem::hard_link_count  
  filesystem::last_write_time  
  filesystem::permissions  
  filesystem::read_symlink  
  filesystem::remove  
  filesystem::remove_all  
  filesystem::rename  
  filesystem::resize_file  
  filesystem::space  
  filesystem::status  
  filesystem::symlink_status  
  filesystem::temp_directory_path

尽管根据 C++ 标准的说明,这些方法在不同的操作系统上行为有些差异,但是 C++ 总算是有了一组统一的接口,再也不用在 C 语言的函数和操作系统的 API 之间来回折腾函数参数类型的转换了。

除了前面介绍的四个主要部分,filesystem 还有很多封装,比如对文件属性的一些强枚举类型定义,比如 file_status 对象,还有从 std::system_error 派生的 std::filesystem::filesystem_error 异常类,除了 what() 方法之外,它还有 path1() 和 path2() 两个方法,可以在异常发生时提供更多的信息。

文件时间和系统时间

无论是 Windows 还是 Linux,系统标记时间都不是我们一般理解的 “年/月/日/时/分/秒” 组成的字符串,而是一个 32 位的计数器(counter)。也就是说,操作系统中的时间,无论是系统时间(system time)还是本地时间(local time),都是一个从格林威治标准时间 1970 年 1 月 1 日 0 时 0 分 0 秒到现在的秒数,计数的单位是“秒”。所以 1970-01-01 00:00:00 UTC 就是操作系统时间纪元(Epoch Time)的起点,而所谓的“本地时间”就是在系统时间的基础上根据操作系统的地区设置做一个时区上的修正。

而文件时间本质上也是一个计数器,它是一个 64 位的计数器,确切地说,是一个从格林威治标准时间 1601 年 1 月 1 日 12 时 0 分 0 秒到现在的以 100 纳秒(nanosecond)为单位的计数器。所以 1601-01-01 12:00:00 UTC 就是文件系统的时间纪元(Epoch Time)的起点。

虽然都是 counter,但是此 counter 非彼 counter,它们之间不能直接比较。操作系统一般会提供两种时间之间的转换接口,但是 C++ 却没有从语言和库的角度提供这样的转换方法。C++ 11 提供了系统时间的库(std::chrono,可以参考之前的文章),并在 C++ 14 进一步完善了一下,使得操作系统时间终于可以不再借助于具体操作系统提供的接口,也摆脱了使用 sprintf 格式化时间的困扰,但是却没有涉及文件时间。C++ 17 的 filesystem 中关于时间的处理看起来也还是个半成品,虽然提供了一个 std::filesystem::file_time_type::clock,但是无论是 std::filesystem 还是 std::chrono,都没有提供它和 std::chrono::system_clock 之间的转换方法。C++ 程序员不得不自己计算出两个时间纪元起点之间的差值,然后修正相应的时间,比如将文件时间转换成系统时间,需要这么实现:

  auto epoch_span = std::filesystem::file_time_type::clock::now().time_since_epoch() - std::chrono::system_clock::now().time_since_epoch();
  auto second_span = std::chrono::duration_cast<std::chrono::seconds>(epoch_span).count();
  auto systime = std::chrono::duration_cast<std::chrono::seconds>(ftime.time_since_epoch()).count() - second_span;

转换的原理很简单,但是实现却非常“丑陋”, 查了一下 C++ 20 的标准,看到它准备用 std::chrono::file_clock 代替 std::filesystem::file_time_type::clock,并且还给出了一个 C++ 20 的实现(概念代码):

  auto systime = decltype(ftime)::clock::to_time_t(ftime);

不过总感觉哪里不对,按道理说,decltype(ftime) 得到的应该就是一个 std::chrono::file_clock,可以直接调用 to_time_t 了,多的那个 ::clock 是什么情况?感觉应该是笔误吧。我个人感觉要是能提供一个 cast 操作符,比如这样:

  auto systime = std::chrono::time_cast<std::chrono::system_clock>(ftime);

会不会更像 C++ 的风格?不过 to_time_t 也行,总算也值得期待了。

从 2003 开始的一点混乱

事实上,boost 库在 2003 年就推出了 filesystem,C++ 17 的标准也是参照了 boost 库,另外,微软的 Visual Studio 2015 也在 C++ 17 的标准正式发布之前,提供了 filesystem 的体验版。由于以上库的内容及其雷同,就使得使用 filesystem 的代码在不同系统上的编译需要稍微考虑一点兼容性。一般来说,不建议直接在代码中直接引用 std::filesystem 或 boost::filesystem,可以考虑用别名来处理这段时间的混乱(从 2003 年到 C++17的发布)。

如果是兼容 Visual Studio 2015 ,可以这样用:

  namespace stdfs = std::experimental::filesystem;

如果是兼容 boost 库,可以这样用:

  namespace stdfs = boost::filesystem;

其他情况:

  namespace stdfs = std::experimental::filesystem;

代码中直接使用 stdfs 就可以了。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK