此文章由机器翻译。

借助 C++ 进行 Windows 开发

借助现代 C++ 使用 Printf

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 和关联的堆栈-­摆弄的宏,但我仍然只承揽的论点。但是不要忽视此解决方案的简单性。再次,编译器会解压函数模板参数,好像我只是曾打电话给 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 + +。

图 2 低级字符串格式设置函数

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