次の方法で共有


Windows と C++

最新の C++ で Printf を使用する

Kenny Kerr

Kenny Kerrprintf を近代化するにはどうすればよいでしょう。C++ には printf に代わる最新の機能が用意されていると思い込んでいる開発者には、この質問が奇妙に感じられるかもしれません。C++ 標準ライブラリが自慢できる点は、優れた標準テンプレート ライブラリ (STL) にあることは間違いありませんが、これにはストリームベースの入出力ライブラリも含まれています。このライブラリは、STL とは大きく異なり、効率性に関わる原理を備えていません。

Alexander Stepanov と Daniel Rose が共同で執筆した『From Mathematics to Generic Programming』 (Addison-Wesley Professional、2015 年) には、「汎用プログラミングとは、効率性を失わずに最も汎用的な設定で機能するように、設計アルゴリズムとデータ構造に重点を置いたプログラミングへのアプローチである」と述べられています。

正直に言えば、printf も cout も決して最新の C++ を代表するものではありません。printf 関数は可変個の引数を受け取る関数の一例で、C プログラミング言語から引き継がれたやや脆弱な機能の数少ない有効な使い方の 1 つです。可変個の引数を受け取る関数は、可変個引数テンプレートより前から存在します。可変個引数テンプレートは、可変個の型や引数を処理するために、まさに最新かつ堅牢な機能を提供しています。これとは対照的に、cout は可変個の引数を使用しませんが、代わりに、仮想関数の呼び出しに大きく依存します。コンパイラは仮想関数呼び出しのパフォーマンスを必ずしも最適化できるわけではありません。実際、CPU 設計の進化は cout のポリモーフィックなアプローチのパフォーマンスを向上することはほとんどなく、printf に有利に働いています。したがって、パフォーマンスと効率性を求めるのであれば、printf を選択するのが賢明です。また、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

ここで当然ある疑問が浮かびます。それぞれの抽象化の出力の有効桁数を変えるにはどうすればよいでしょう。小数点の右側に 2 桁だけを出力したいとします。printf では、浮動小数点変換指定子の一部として簡単に有効桁数を指定できます。

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

これで数値が丸められ、次の結果が出力されます。

123.46

cout で同じ結果を得るには、もう少し入力が必要です。

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

このような冗長さがまったく気にならず、むしろ柔軟性や表現力を楽しんでいるとしても、この抽象化にはコストが伴います。まず、マニピュレーターの fixed と setprecision はステートフルです。つまり、その効果は元に戻されるかリセットされるまで保持されます。一方、printf の変換指定子には 1 回の変換に必要な指定のみが含まれ、他のコードには影響しません。その他のコストはほとんどの出力にとって問題にはなりませんが、他のユーザーのプログラムが自身のプログラムよりも何倍も高速に出力されると分かれば問題になるかもしれません。仮想関数の呼び出しに伴うオーバーヘッドとは別に、endl にも予想以上の負荷がかかります。endl は改行を出力に送信するだけでなく、基になるストリームが出力をフラッシュする原因にもなります。コンソール、ディスク上のファイル、ネットワーク接続、グラフィックス パイプラインのいずれであっても、I/O に何かを書き込むときにフラッシュが行われると、通常、コストは非常に高くなり、それが繰り返されればパフォーマンスが低下するのは間違いありません。

printf と cout の違いについて少し説明したところで、最初の疑問に戻りましょう。printf を近代化するにはどうすればよいでしょう。最新の C++ (C++11 以降など) の登場により、パフォーマンスを犠牲にすることなく printf の生産性と信頼性を向上できることは間違いありません。printf とは直接関係ありませんが、C++ の標準ライブラリには言語の公式メンバーとして string クラスがあります。このクラスも長年評判が良くなかったのですが、優れたパフォーマンスを発揮するようになりました。問題がないわけではありませんが、C++ で文字列を処理するのに非常に有用な方法を提供します。そのため、printf の近代化は、string と wstring をうまく使えばなんとかなるはずです。では、何ができるかを見ていきます。まず、printf に関して最も厄介と考えられる問題から対処しましょう。

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

このコードは実際には機能すべきですが、一目みて明らかなように、「未定義の動作」と呼ばれる結果になります。printf の結果は渡されるテキスト次第です。そして、C++ 言語でのテキストの基本表現が C++ の string クラスです。必要なことは、これが機能する方法で 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 関数を次のように呼び出したとしたらどうでしょう。

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

123 と 456 で構成される args パラメーター パックは、可変個引数テンプレートの本体内部で展開されます。これは、次のように記述したのと同じ結果になります。

      

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

では、どのようなメリットがあるでしょう。vprintf ではなく printf を呼び出しているので、va_list マクロとそれに関連付けられる stack-­twiddling マクロを管理する必要がありません。それでも、引数を渡すだけですんでいます。このソリューションの簡潔さを見落とさないでください。繰り返しになりますが、単純に printf を直接呼び出ししているかのように、コンパイラは関数テンプレートの引数をアンパックします。つまり、この方法では printf をラップするオーバーヘッドが生じません。これは最高級の C++ としての特性を残したまま、言語の強力なメタプログラミング手法を使用して、完全に汎用の方法で必要なコードを挿入できることを意味します。単に args パラメーター パックを展開するのではなく、各引数をラップして、printf に必要な調整を加えることができます。次のシンプルな関数テンプレートについて考えてみます。

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

多くのことを行っているように見えませんし、実際にそのとおりなのですが、これにより、次のように、パラメーター パックを展開してこの Argument 関数の 1 つに各引数をラップすることができるようになります。

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++ の string クラスを処理するように Argument 関数をオーバーロードできるようになります。

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

Print 関数テンプレートでは、書式設定されない出力用のオーバーロードも多数使用します。これにより、安全性が高まり、任意の文字列を変換指定子を含んでいるかのように printf が誤って解釈するのを防ぎます。図 1 にこのようなオーバーロード関数を一覧します。

図 1 書式設定されない出力用の Print

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());
}

最初の 2 つのオーバーロードは、通常文字の配列とワイド文字の配列をぞれぞれ書式設定します。最後の関数テンプレートは、引数に string と wstring のどちらが指定されているかに応じて、適切なオーバーロードに転送します。このようなオーバーロード関数を用意すれば、次のように、いくつかの変換指定子をそのとおりに安全に出力できます。

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

これは、文字列の出力を安全かつ特別に意識することなく処理することによって、printf に対するよくある不満を解消します。では、文字列自体の書式設定はどうでしょう。C++ の標準ライブラリには、文字列バッファーへの書き込みを目的として、printf のさまざまなバリエーションが用意されています。これらのバリエーションのうち、最もよく使われているのは snprintf と swprintf だと考えます。この 2 つの関数はそれぞれ、通常文字とワイド文字の出力を処理します。どちらも書き込み可能な最大文字数を指定し、元のバッファーの大きさが不十分な場合に必要な容量を求めるのに使用する値を返すことができます。しかし、これらの関数自体は間違いを起こしやすく、使用が非常に面倒です。ここにも最新の C++ を使用します。

C は関数のオーバーロードをサポートしていませんが、C++ ではオーバーロードをかなり有効に使用でき、汎用プログラミングを可能にしています。そのため、snprintf と swprintf の両方を StringPrint 関数にラップすることから始めます。また、可変個引数関数テンプレートも使用して、先ほど Print 関数に使用した安全な引数展開を利用できるようにします。図 2 に、両方の関数のコードを示します。これらの関数は、結果が -1 以外になることをアサートします。-1 は、書式文字列の解析時に基になる関数で回復できない問題が発生した場合に返されます。これはバグなので、運用コードをリリースする前に修正しなければならないため、アサーションを使用します。これを例外に置き換えることを考えるかもしれませんが、未定義の動作につながる無効な引数が渡される可能性がある限り、あらゆるエラーを例外に変換する万全の方法はありません。最新の 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 関数に依頼しているだけです。通常どおり、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 はどちらも、報告されるバッファー サイズに Null 終端文字の領域が含まれると想定されるためです。これは正しく機能しますが、明らかにパフォーマンスを意識していません。ほとんどの場合にもっと高速になるアプローチは、書式設定された出力を格納し、必要なときのみサイズを変更すればよいように、文字列が十分に大きいと想定することです。これは、ほぼ上記のコードを置き換えたものですが、安全性はきわめて高くなります。まず、次のように、文字列をバッファーに直接書式設定します。

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 関数から戻るだけです。Format 関数テンプレート全体を、図 3 に示します。string クラスを使い慣れていると、クラスがその容量を報告することをご存知でしょう。そのため、最初から文字列を正しく書式設定できる可能性を高めるために、StringPrint を呼び出す前にその容量に相当する文字列サイズを設定することを考えるかもしれません。問題は、printf がその書式文字列を解析して必要なバッファー サイズを計算するよりも速く、文字列オブジェクトのサイズを変更できるかどうかです。非公式にテストを行ってみましたが、「場合によります」という答えになります。文字列のサイズをその容量に合わせて変更する場合、報告されるサイズを単に変更する以上のことが必要になります。余分な文字をクリアする必要があり、これには時間がかかります。printf が書式設定文字列を解析するよりも時間かかるかどうかは、クリアする文字数と書式設定の複雑さによって変化します。大量の出力にはもっと高速なアルゴリズムの使用を考えますが、大半のシナリオでは、図 3 の Format 関数が優れたパフォーマンスを提供することがわかりました。

図 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);
  }
}

この Format 関数を用意すれば、文字列書式設定の一般的な操作を行うさまざまなヘルパー関数を非常に簡単に記述できるようになります。おそらく、ワイド文字列を通常文字列に変換することが必要になるでしょう。

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++ プログラミング技法を刷新する方法について思いつかれることを願っています。ちなみに、出力ライブラリでは、入れ子にした Internal 名前空間で Argument 関数と低レベルの StringPrint 関数を定義しています。これにより、ライブラリを使いやすく調べやすい状態にしていますが、必要に応じて独自に実装を調整できます。


Kenny Kerr は、カナダを拠点とするコンピューター プログラマであり、Pluralsight の執筆者です。また、Microsoft MVP でもあります。彼のブログは kennykerr.ca (英語) で、Twitter は twitter.com/kennykerr (英語) でフォローできます。