1.34 (國際化) 變更

簡介

這一版本是 Filesystem 庫的一個重要升級,為提交到C++標準委員會作好準備。本版本的特性包括:

還提供了這些變更的 原理

國際化

類模板 basic_path, basic_filesystem_error, 和 basic_directory_iterator 提供了國際化的基本機制,與C++標準庫的 basic_string 及相似的類模板一樣。提供了以下 typedefs:

typedef basic_path<std::string, ...> path;
typedef basic_path<std::wstring, ...> wpath;

typedef basic_filesystem_error<path> filesystem_error;
typedef basic_filesystem_error<wpath> wfilesystem_error;

typedef basic_directory_iterator<path> directory_iterator;
typedef basic_directory_iterator<wpath> wdirectory_iterator;

Boost.Filesystem basic_path 所使用的字符串類型(std::string, std::wstring, 或別的什麼)被稱為內部字符串類型。操作系統用於路徑表示的字符串類型(通常是 char*, 有時是 wchar_t*)則稱為外部字符串類型。內部和外部類型間的轉換是通過路徑 traits 類來執行的。對於 pathwpath 的特定轉換是實現定義的,為了標準化建議使用操作系統的首選文件系統編碼。對於許多現代的POSIX-based文件系統,wpath 的外部編碼是 UTF-8, 而對於現代 Windows 文件系統如 NTFS 則是 UTF-16.

operations.hpp操作函數 分別提供了 path, wpath, 和用戶自定義 basic_path 的重載。具體實現應遵循"do-the-right-thing" 的規則,確認正確的重載可以被選中。

簡化 path 接口

本庫的早期版本要求類 path 的用戶確定路徑的格式(原生或通用的)以及名字錯誤檢查策略,通過第二個構造函數參數或是缺省的機制。這一方法引起了抱怨,特別是那些不需要名字檢查特性的用戶。現在該接口已經簡化了:

另外,basic_filesystem_error 已被提上議程進行簡化。

錯誤代碼被移至一個獨立的庫 Boost.System 中。

"//:" 被作為一個路徑轉義前綴,用於標識原生路徑。原理:簡化 basic_path 的構造函數接口,更易於在需要顯式原生格式標識的平台上使用。

謂詞函數的合理化

在 Boost 開發者郵件列表中的討論和缺陷報告中,Boost.Filesystem 的 exists(), symbolic_link_exists(), 和 is_directory() 謂詞函數顯然不夠。有人建議增加 is_accessible() 函數,但 Peter Dimov 認為這只是在缺乏明確規格的問題之上糊上一層紙,很可能會導致未來的問題。

Peter 建議了一個有趣的方法來分析這個問題,即詢問對於不同謂詞的 true 和 false 值有什麼期望。請見以下 表格

status()

作為謂詞討論的一部分,特別是和 Rob Stewart 的討論,很明顯有時候應用程序需要訪問原始的狀態信息而不能拋出任何異常。函數 status() 被增加以滿足這一要求。根據 status() 指定謂詞函數的語義也被證明是更清晰的。

is_file()

同一時間,Jeff Garland 建議了 is_file() 謂詞和 is_directory(). 在以下分析中,顯然對 is_file() 的期望值不同於對 !is_directory() 的期望值,所以就增加了 is_file().

is_other()

在一些操作系統上,可能有一個目錄項既不是目錄也不是文件。函數 is_other() 可以標識出這種情況。

謂詞在錯誤時應該拋出嗎?

由操作系統作為錯誤所報告的一些情況(參見 腳注)顯然只是表示謂詞為假,而不是表示嚴重錯誤。但其它的錯誤則表示嚴重的硬件或網絡問題,或是權限問題。

有的人,尤其是 Stewart, 認為在象 is_directory() 這樣的函數中,任何錯誤都只應導致函數返回 false. 如果真的存在底層的問題,在嘗試使用 directory_iterator 或 fstream 操作時就會檢測出來。

這種觀點被拒絕,因為以下考慮:

不過,討論也確實表明會存在需要不拋出行為的情況,程序員可能更願意在一個更低的掩碼層次來處理文件或目錄屬性和錯誤。函數 status() 被提議以滿足這一需要。

異常表

在下表中,p 為非空路徑。

除非另外指出,所有函數都在硬件或常見錯誤、權限或訪問錯誤、符號鏈接循環錯誤和無效路徑錯誤時拋出異常。如果操作系統無法區分錯誤類型,則謂詞操作在不明錯誤時返回 false.

期望 表示操作被期望成功或失敗,假設沒有硬件、權限或訪問訪問,也沒有競爭條件。

表達式 期望 語義
is_directory(p) 返回 true 如果 p 被找到且為目錄,否則返回 false.
如果 true, 則 directory_iterator(p) 將成功。
如果 false, 則 directory_iterator(p) 將失敗。
拋出: 如果 status() & error_flag
返回: status() & directory_flag
is_file(p) 返回 true 如果 p 被找到且不是目錄,否則返回 false.
如果 true, 則 ifstream(p) 將成功。
不過 False 並不表示 ifstream(p) 就會失敗(因為有些操作系統允許目錄被以文件方式打開,但 stat() 會設置"常規文件"標誌)。
拋出: 如果 status() & error_flag
返回: status() & file_flag
exists(p) 返回 is_directory(p) || is_file(p) || is_other(p) 拋出: 如果 status() & error_flag
返回: status() &   (directory_flag|file_flag|other_flag)
is_symlink(p) 返回 true 如果 p 在淺查找(非傳遞的)中被找到,且為一個符號鏈接,否則返回 false.
如果 true, 且 p 指向 q, 則對於任意文件系統函數 f,除了特別聲明以淺方式操作符號鏈接的以外,f(p) 調用 f(q), 並返回由 f(q) 返回的任何值。
拋出: 如果 symlink_status() & error_flag
返回: symlink_status() & symlink_flag
!exists(p) && ((p.has_branch_path() && exists( p.branch_path()) || (!p.has_branch_path() && !p.has_root_path()))
換言之,如果路徑不存在且 (分枝存在,或 (沒有分枝且沒有根)).
如果 true, 則 create_directory(p) 將成功。
如果 true, 則 ofstream(p) 將成功。
 
 
directory_iterator it(p) 如果 it != directory_iterator(), assert(exists(*it)||is_symlink(*it)). 注: exists(*it) 可能會拋出,同樣 status(*it) 也可能返回 error_flag - 對可訪問性沒有保證。  

結論

謂詞操作 is_directory(), is_file(), is_symlink(), 和 exists() 帶有明確的語義,形成一個符合期望的一致集合。

保留已有用戶代碼

雖然改為基於模板的方法要求對實現進行完全的檢查,但是已有代碼所使用的接口大部分保持不變。通過提供一些不贊成使用的函數,轉換的問題對用戶代碼的影響已得到減少,更容易進行轉換。不贊成使用的函數有:

// class basic_path - 忽略構造函數的第二個參數:
basic_path( const string_type & str, name_check );
basic_path( const typename string_type::value_type * s, name_check );

// class basic_path - 為更名函數提供的舊名字:
string_type native_file_string() const;
string_type native_directory_string() const;

// class basic_path - 現在被定義為沒有任何的實際作用:
static bool default_name_check_writable() { return false; }
static void default_name_check( name_check ) {}
static name_check default_name_check() { return 0; }

// class path 的不可推導的操作函數
inline path current_path()
inline const path & initial_path()

// 新的 basic_directory_entry 提供了 leaf()
// 用於覆蓋已有的常見用例 itr->leaf()
typename Path::string_type leaf() const;

如果你不想包含這些不贊成使用的函數,請定義宏 BOOST_FILESYSTEM_NO_DEPRECATED.

對於已有代碼最大的影響是,目錄迭代器的值類型由 path 變為 directory_entry. 為了最常見的目錄迭代器用例更為易用,basic_directory_entry 提供了一個到 basic_path 的自動轉換,並可以防止破壞大量的已有代碼。有關原理的討論請見 下一節

// 新的 basic_directory_entry 提供了:
operator const path_type &() const;

遍歷目錄時更為高效的操作

真實世界中幾個最常用的操作系統(BSD 系, Linux, Windows) 提供了在目錄遍歷時的狀態信息。對這些狀態信息進行緩存,可以將典型的謂詞操作提高3-6倍的速度。(對於一個包含15,047個文件的目錄,在剛啟動的 系統上,遍歷時間是1秒對6秒,對於使用過的目錄,則是0.3秒對0.9秒)。

從緩存這些狀態信息所獲得的效率提升是非常顯著的,不能忽略。因為存在不同競爭條件的可能性,取決於是使用緩存的信息或是執行實際的系統調用,有必要考慮提供顯式的函數來使用緩存的信息,而不是在幕後暗中使用緩存信息。

有三個選項用於獲取緩存的狀態信息,每一個都有完整的實現。開始在實現了選項1以後,發現了下面所列的問題,然後測試了選項2,作為工程上的權衡。最後選項3被作為最乾淨的設計。

選項 如何訪問緩存 優缺點
1 謂詞函數重載
(basic_directory_iterator value_type 為 path)
  • 非常成問題的設計(濫用友元,濫用重載,等等)
  • 用戶不能重用緩存
  • 可讀性問題;很容易混淆 f(*it) 和 f(it)
  • 可寫性問題(容易出錯?)
  • 最常用的迭代器主要是: *it
  • 保護已有代碼
2 basic_directory_iterator
 的謂詞成員函數(basic_directory_iterator value_type 為 path)
  • 較乾淨的設計(雖然新增的迭代器函數不常用)
  • 用戶不能重用緩存
  • 可讀性和可寫性都OK: f(*it) 和 it.f() 明顯不同
  • 最常用的迭代器主要是: *it
  • 保護已有代碼
3 basic_directory_entry
 的謂詞成員函數(basic_directory_iterator value_type 為 basic_directory_entry)
 
  • 最乾淨的設計。
  • 用戶可以重用緩存。
  • 可讀性和可寫性都OK: f(*it) 和 it->f() 明顯不同。
  • 最常用的迭代器更長: it->path(), 不過由於提供了"operator const basic_path &",所以也可以只寫 *it.
  • 破壞了一些已有代碼。"operator const basic_path &" 轉換消除了最常見情況的破壞,而提供了一個(不贊成使用) leaf() 則防止了次常見情況的破壞。

原理

消除了原生格式與通用格式的差異

消除原生路徑格式和通用路徑格式的差異的最早動機是消除用戶的混亂和簡化泛型設計。

在設計工作的過程中,發現了進一步的技術論點。考慮路徑 "c:foo/bar". 在多數 POSIX 系統上,"c:foo" 是一個有效的目錄名,所以我們有一個兩元素的路徑,原生格式與通用格式相比沒有問題。但是在 Windows 系統上,"c:" 是一個驅動器符,所以我們有一個三元素的路徑。所以對操作系統的調用都會把 "c:" 視為一個驅動器符;通過聲明該格式是通用格式也不能改變事實。所以原生格式和通用格式的差異是沒有用處的,還會在 POSIX, Windows, 和其它操作系統間造成誤解。

如果某個特定操作系統的路徑確實存在差異,它可以通過要求原生路徑格式以某種唯一標識作為前綴來解決。例如 "native-path:". 這一要求僅適用於以下條件的操作系統:(1) 存在差異,且(2) 沒有辦法從字面上區別兩種格式。例如,原生的操作系統使用的原生格式與 Filesystem 庫的類似於 POSIX 的通用格式相同,但操作系統是從右向左處理元素,而不是從左向右。

保護已有代碼

允許已有的用戶代碼可以和庫的升級版本一起工作顯然是有好處的,這樣可以保護用戶在學習和使用本庫編寫代碼上已經付出的努力。

另外還有一個動機;除了 class path 的名字檢查部分以外,現有接口已被證實是可用的和牢固的,沒有理由去亂動它們。

單路徑設計

在 Boost 開發者列表中的對國際化的初步討論中,提出了一個設計,即使用單個路徑類來同時保存基於窄字符和寬字符的路徑。但由於以下原因,該設計被拒絕了:

沒有在發生錯誤時拋出異常的 status() 版本

不提供一個在發生錯誤時拋出異常的 status() 版本的原理是,(1) 該函數的主要目的是在一個非常低的層次執行查詢,通常在這些層次上是不希望發生異常的,且 (2) 謂詞函數已經提供了發生錯誤時拋出異常的功能。提供一個拋出版本的 status() 只有很少或沒有效率的提高。

status() 函數的符號鏈接標識版本

status() 函數的符號鏈接標識版本是由第二個參數進行區分的。通常,在行為不同時,單獨的命名函數要比重載更合適,這裡的情況就是這樣,而重載更適用於行為相同但參 數類型不同的情形(Iain Hanson). 在這個特殊情況下選擇使用重載是因為主觀的判斷,我們認為帶一個可選的 "symlink" 參數的相同函數名可以產生更易理解的代碼。該函數的原先實現使用了名字 "symlink_status", 但是在實際代碼中看起來不太適合。

POSIX wpath_traits 缺省為 locale(""), 但允許注入 locale

Vladimir Prus 指出,對於 Linux (可能包括其它 POSIX 操作系統),需要將寬字符路徑轉換為窄字符,缺省的轉換不應只依賴於操作系統,而應該取決於缺省的 std::locale(""). 例如,在 Linux(及 Russian 網站)上,常用的 Russian 編碼是 KOI8-R (RFC1489). 同時還提供了安全指定一個不同 locale 的能力,以滿足不可預見的需要。


Revised 18 March, 2008

Ac Copyright Beman Dawes, 2005

Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at www.boost.org/LICENSE_1_0.txt)