本文章是由機器翻譯。

Windows 與 C++

使用 Printf 與現代化的 C++

Kenny Kerr

Kenny Kerr它會採取現代化 printf?這可能看起來像一個奇怪的問題,對許多開發人員認為 c + + 已經提供了 printf 現代替代。雖然 c + + 標準庫的成名無疑是優秀的標準範本庫 (STL),它還包括一個基於工作流的輸入 /­輸出庫 stl 沒有相似之處,體現了其原則中沒有與效率有關。

"泛型程式設計是程式設計方法,重點設計演算法和資料結構,以便他們在不損失效率,帶的最一般設置工作"亞歷山大諾夫和 Daniel 的玫瑰,在書中,"從數學到泛型程式設計"(艾迪生-Wesley 專業,2015年)。

老實說,printf 既 cout 是以任何方式代表現代 c + +。Printf 函數是功能的可變參數函數的一個示例和幾個好地利用了從 C 程式設計語言繼承此有點脆之一。可變函數要早于可變參數範本。後者提供了一個真正現代和魯棒性的設施處理 ; 類型或參數的數目可變。與此相反的是,cout 不使用可變參數調用任何東西,而是如此嚴重依賴虛擬函式呼叫編譯器不能做太多以優化其性能。事實上,CPU 設計的演變備受青睞 printf,但卻不會提高 cout 的多態方法的性能。因此,如果你想要性能和效率,printf 是更好的選擇。它也產生了更簡潔的代碼。下面是一個示例:

#include <stdio.h>
int main()
{
  printf("%f\n", 123.456);
}

%F 轉換說明符告訴 printf 期望一個浮點數,並將其轉換為十進位表示形式。\N 是只是一個普通分行符號字元,可能擴大到包括回車,根據目的地。浮點轉換假定一個精度為 6,指的將小數點後顯示的小數位數的數目。因此,此示例將列印以下的字元後, 跟一個新行:

123.456000

實現同端 cout 似乎相對順直­起初轉發:

#include <iostream>
int main()
{
  std::cout << 123.456 << std::endl;
}

在這裡,cout 依賴運算子多載來直接或發送到輸出流的浮點數。我不喜歡濫用的運算子多載以這種方式,但我承認它是一種個人風格。Endl 最後的輸出流中插入一個新行。然而,這並非 printf 示例完全相同,而且與不同的小數精度的輸出:

123.456

這會導致一個顯而易見的問題:如何更改精度的各自的抽象?好吧,如果我要的只是兩個小數點後面的位數,我可以簡單地指定這為 printf 浮點數轉換說明符的一部分:

printf("%.2f\n", 123.456);

現在 printf 將輪的編號,以產生以下結果:

123.46

要獲得相同的效果與 cout,需要一點更多的打字:

#include <iomanip> // Needed for setprecision
std::cout << std::fixed << std::setprecision(2)
          << 123.456 << std::endl;

即使你不介意冗長的所有這一切,而享受的靈活性或表現力,請記住,這種抽象是需要付出代價。首先,固定和 setprecision 機器人是無狀態的含義及其影響仍然存在,直到他們是逆轉或重置。相比之下,printf 轉換說明符包括一切所需的那種單個的轉換,而不會影響任何其他代碼。另一個的成本可能並不重要,對於大多數的輸出,但這一天可能會到來,當你注意到別人的程式可以輸出可以比你快好多倍。除了從虛函式呼叫的開銷,endl 也給你更多你可能有指望的。不僅它會發送一個新行輸出中,而且它還會導致要刷新其輸出的基礎流。編寫任何類型的 I/O,是否到主控台,一個檔在磁片、 網路連接或甚至圖形管道,沖洗時通常價格昂貴,並一再的刷新無疑會嚴重影響性能。

我探討和對比 printf 和 cout 一點,現在是時間來恢復到原來的問題:它會採取現代化 printf?當然,隨著現代 c + +,例證的 C + + 11 及以後,可以改進生產力和可靠性的 printf 而不犧牲性能。另外一個有點無關的 c + + 標準庫成員是該語言的官方字串類。雖然此類也已經被誤用多年來,它確實提供優異的性能。雖然不是沒有錯,它提供了非常有用的方法來處理 c + + 中的字串。因此,任何現代化的 printf 真的應該與字串和 wstring 玩得好。讓我們看看可以做些什麼。首先,讓我談談我認為是的 printf 最令人頭痛的問題:

std::string value = "Hello";
printf("%s\n", value);

這真的應該去工作,但我敢肯定你可以清楚地看到,相反,它將導致在什麼被親切地稱為"未定義的行為"。正如你所知,printf 是文字的所有關于文本和 c + + 字串類是文字的 c + + 語言的卓越表現。需要做的什麼是包裹在這樣的 printf 這只是工作的方式。我不想要反復拔掉的字串為 null 終止字元陣列,如下所示:

printf("%s\n", value.c_str());

這是只是單調乏味,所以我要去修理它通過環繞 printf。傳統上,這涉及編寫另一個可變參數的函數。也許這樣的事情:

void Print(char const * const format, ...)
{
  va_list args;
  va_start(args, format);
  vprintf(format, args);
  va_end(args);
}

不幸的是,這一無所獲,我。它可能有用來包裝的 printf 的一些變體才能寫入一些另一個緩衝區,但在這種情況下,我已經獲得了什麼值錢的東西。我不想回到可變參數的 C 樣式函數。相反,我想要向前看,擁抱現代 c + +。幸運的是,由於 C + + 11 支援可變數量範本,我再沒有寫我生命中的另一個可變函數。而不是環繞在另一個可變函數 printf 函數,我可以轉而把它裹可變參數範本:

template <typename ... Args>
void Print(char const * const format,
           Args const & ... args) noexcept
{
  printf(format, args ...);
}

起初,它似乎並不認為我已經獲得了很多。要是來調用列印函數像這樣:

Print("%d %d\n", 123, 456);

它將導致 args 參數包,由 123 和 456,擴大可變參數範本體內仿佛只是寫了這樣組成的:

      

printf("%d %d\n", 123, 456);

所以我得到了什麼?當然,我打電話 printf,而不是 vprintf,我不需要管理 va_list 和關聯的堆疊-­擺弄的宏,但我仍然只承攬的論點。Don不但是忽略此解決方案的簡單性。再次,編譯器會解壓函數範本參數,好像我只是曾打電話給 printf 直接,這意味著在以這種方式環繞 printf 沒有開銷。這也意味著這是仍然一流的 c + +,我還可以使用該語言的強大的元程式設計技術來注入任何所需的代碼 — — 完全通用的方式。而不是簡單地擴大 args 參數包,我可以包裝每個參數添加所需的 printf 的任何調整。考慮這個簡單的函數範本:

template <typename T>
T Argument(T value) noexcept
{
  return value;
}

看來不做太多,事實上不是這樣,但我現在可以展開參數包來包裝每個參數在一個這些函數中,如下所示:

template <typename ... Args>
void Print(char const * const format,
           Args const & ... args) noexcept
{
  printf(format, Argument(args) ...);
}

我仍然可以在相同的方式調用 Print 函數:

Print("%d %d\n", 123, 456);

但它現在有效地產生以下擴展:

printf("%d %d\n", Argument(123), Argument(456));

這是非常有趣。當然,對於這些整數參數,都無所謂,但我現在可以重載參數函數來處理 c + + 字串類:

template <typename T>
T const * Argument(std::basic_string<T> const & value) noexcept
{
  return value.c_str();
}

然後我可以簡單地稱為 Print 函數與某些字串:

int main()
{
  std::string const hello = "Hello";
  std::wstring const world = L"World";
  Print("%d %s %ls\n", 123, hello, world);
}

編譯器將有效地擴大內部 printf 函數,如下所示:

printf("%d %s %ls\n",
  Argument(123), Argument(hello), Argument(world));

這將確保每個字串為 null 終止字元陣列提供給 printf 並產生完全明確的行為:

123 Hello World

以及列印函數範本,我也用大量的重載為未格式化輸出。這往往是更安全,並防止 printf 意外地曲解為包含轉換說明符的任一字元串。圖 1 列出了這些函數。

圖 1 列印格式化的輸出

inline void Print(char const * const value) noexcept
{
  Print("%s", value);
}
inline void Print(wchar_t const * const value) noexcept
{
  Print("%ls", value);
}
template <typename T>
void Print(std::basic_string<T> const & value) noexcept
{
  Print(value.c_str());
}

前兩個重載只是格式普通和寬字元陣列,分別。最後函數範本將轉發到適當的重載,具體取決於是否作為參數提供的字串或 wstring。鑒於這些函數,我可以安全地一些轉換說明符,列印從字面上,具體如下:

Print("%d %s %ls\n");

通過處理字串輸出安全和透明的方式,照顧 printf 我最常見的不滿。怎麼樣格式字串本身嗎?C + + 標準庫提供不同的變形的 printf 寫入字元的字串緩衝區。其中,我發現 snprintf 和 swprintf 最有效。這兩個函數分別處理字元和寬字元輸出。它們允許您指定的最大數目可能寫並返回一個值,可以用來計算需要多少空間,如果原始的字元緩衝區不會足夠大。儘管如此,靠自己他們是容易出現錯誤和相當乏味的使用。一些現代的 c + + 的時間。

C 不支援函數重載,它是更加方便的使用 c + + 中重載而這開門是泛型程式設計,所以我會開始通過包裝 snprintf 和 swprintf 作為調用 StringPrint 函數。我還將使用可變參數函數範本,可以讓我以前用於列印功能的安全參數擴張的優勢。圖 2 提供兩個函數的代碼。這些函數還斷言結果不是-1,這是底層函數所返回的一些可恢復的問題,解析格式字串時。我使用斷言,因為我只是覺得這是一個 bug,應固定在航運生產代碼之前。你可能想要此替換為一個異常,但請牢記沒有防彈的所有錯誤都變成例外,因為它是仍然有可能會導致未定義行為的無效參數傳遞方式。現代 c + + 不是白癡的 c + +。

圖 2Low-水準字串格式的函數

template <typename ... Args>
int StringPrint(char * const buffer,
                size_t const bufferCount,
                char const * const format,
                Args const & ... args) noexcept
{
  int const result = snprintf(buffer,
                              bufferCount,
                              format,
                              Argument(args) ...);
  ASSERT(-1 != result);
  return result;
}
template <typename ... Args>
int StringPrint(wchar_t * const buffer,
                size_t const bufferCount,
                wchar_t const * const format,
                Args const & ... args) noexcept
{
  int const result = swprintf(buffer,
                              bufferCount,
                              format,
                              Argument(args) ...);
  ASSERT(-1 != result);
  return result;
}

StringPrint 函數提供字串格式設置的處理的一般的方法。現在我可以專注于 string 類的細節,這主要涉及到記憶體管理。我想寫這樣的代碼:

std::string result;
Format(result, "%d %s %ls", 123, hello, world);
ASSERT("123 Hello World" == result);

那裡是沒有可見緩衝區管理。我沒有想出多大的緩衝區來分配。我只是問 Format 函數格式化的輸出邏輯分配的字串物件。像往常一樣,格式可以是一個函數範本,具體可變參數範本:

template <typename T, typename ... Args>
void Format(std::basic_string<T> & buffer,
            T const * const format,
            Args const & ... args)
{
}

有很多種方式來實現此功能。一些試驗和一劑好的貌相走很長的路。一個簡單而幼稚的方法是假設該字串為空或太小,無法包含格式化的輸出。在這種情況下,我會先確定所需的大小與 StringPrint,若要匹配,將緩衝區大小調整,然後調用 StringPrint 再次與妥善分配的緩衝區。這樣的事情:

size_t const size = StringPrint(nullptr, 0, format, args ...);
buffer.resize(size);
StringPrint(&buffer[0], buffer.size() + 1, format, args ...);

+ 1 是必需的因為 snprintf 和 swprintf 假定報告的緩衝區大小包括空結束字元的空間。這工作的很好,但它應該是顯而易見的在桌子上我決定離開性能。在大多數情況下更快的方法是假定字串是大到足以包含格式化的輸出,只有在必要時調整其大小。這幾乎反轉前面的代碼,而是相當安全。首先,我試圖直接到緩衝區格式的字串:

size_t const size = StringPrint(&buffer[0],
                                buffer.size() + 1,
                                format,
                                args ...);

如果字串是空的開頭或只是沒有足夠大,所得到的大小將會遠遠大於字串的大小和我就知道要調整再次調用 StringPrint 之前的字串:

if (size > buffer.size())
{
  buffer.resize(size);
  StringPrint(&buffer[0], buffer.size() + 1, format, args ...);
}

如果所得到的大小小於字串的大小,我就知道格式化成功,但緩衝區需要被修整,以匹配:

else if (size < buffer.size())
{
  buffer.resize(size);
}

最後,如果大小匹配沒事可做,Format 函數可以簡單的返回。完整的格式函數範本可以發現在圖 3。如果您熟悉使用 string 類,您可能還記得,它還報告其能力和你可能試圖設置的字串大小來匹配其容量在首次調用 StringPrint 之前思考這可能改善你的格式化字串正確第一次機會。問題是,是否可以更快,比 printf 可以解析它的格式字串,並計算所需的緩衝區大小調整一個字串物件。基於我非正式的測試中,答案是:它取決於。你看,調整大小來匹配其容量的字串超過只不過修改報告的大小。必須清除任何額外的字元,這需要時間。這是否需要更多時間比 printf 來解析它的格式字串取決於多少個字元需要被清除並且多麼複雜的格式碰巧。我使用更快的演算法為高-­音量輸出,但發現在 Format 函數圖 3 提供了良好的性能,對於大多數情況。

圖 3 格式字串

template <typename T, typename ... Args>
void Format(std::basic_string<T> & buffer,
            T const * const format,
            Args const & ... args)
{
  size_t const size = StringPrint(&buffer[0],
                                  buffer.size() + 1,
                                  format,
                                  args ...);
  if (size > buffer.size())
  {
    buffer.resize(size);
    StringPrint(&buffer[0], buffer.size() + 1, format, args ...);
  }
  else if (size < buffer.size())
  {
    buffer.resize(size);
  }
}

使用此格式函數手裡,它也變得非常容易寫字串格式設置的常見操作的各種 helper 函數。也許你需要寬字元字串轉換為普通的字串:

inline std::string ToString(wchar_t const * value)
{
  std::string result;
  Format(result, "%ls", value);
  return result;
}
ASSERT("hello" == ToString(L"hello"));

也許你需要浮點數字格式:

inline std::string ToString(double const value,
                            unsigned const precision = 6)
{
  std::string result;
  Format(result, "%.*f", precision, value);
  return result;
}
ASSERT("123.46" == ToString(123.456, 2));

為性能癡迷,這種專門的轉換函數也是很容易進一步優化,因為所需的緩衝區大小是可以預測的但我會離開,你要靠你自己摸索的。

這是幾個有用的功能,從我現代的 c + + 輸出庫。我希望他們給你一些如何使用現代 c + + 來更新一些老派的 C 和 c + + 程式設計技術的靈感。順便說一句,我輸出庫定義參數的功能,以及底層的 StringPrint 函數中嵌套的內部命名空間。這傾向于保持圖書館,美好而簡單的發現,但您可以安排您的實現,但是你希望。


Kenny Kerr 是一個位於加拿大,以及作者 Pluralsight 和微軟最有價值球員的電腦程式員。他的博客 kennykerr.ca ,你可以跟著他在 Twitter 上 twitter.com/kennykerr