「printf」の功罪と「iostream」
C++ 初心者の頃 C 言語から C++ に移行して、大きな驚きを感じたのは、文字の入出力関係でしょうか?
現在 C++ でプログラムを作成するのが「常」となり、結論から言わせてもらうと、C++ では、もはや「printf」を使う理由は全く無い事です。
今「え?」と言った人は、C++ にまだ移行できていない中途半端な状態だと思われます。
printf("Hello !!!\n");
では無くて、
std::cout << "Hello !!!" << std::endl;
です!
C++ に馴染みの無い人には、今まで観た事の無いような、記述で、凄く奇妙に写ると思います。
C++ ではオペレーターをオーバーロード出来る為、「<<」を「シフト」では無く、全く別の意味で使う事が出来ます。
「iostream」クラスでは、cout(stdout)オブジェクトに対して、文字列を流し込む事で、文字の表示を行う事が出来るような設計になっています。
※入力は、std::cin オブジェクトから「>>」で行えます。
以前にとあるプログラマーが「iostream はわかりずらいので使う価値が無い」的な事を言っていましたが、甚だしい勘違いです。
「使い易い」、「使いずらい」と言う感覚は、単純に個人の「慣れ」の問題であって、その感覚だけで「扱いづらい」と結論してしまう事に危機的な危うさを感じます。
多分、iostream を設計した人々は、printf に関連する諸問題にとっくの昔に気がついていて、それを避け、尚且つ簡単に扱えるようにするにはどうしたら良いかを長い時間考えたり、ディスカッションして、現在の実装になったと思います。
iostream は、printf に比べると速度が遅い(負荷が大きい)と言うのはあるかもしれませんが、printf に比べると、安全で、プログラマーが受けられる恩恵が大きい優れた物です、それを良く理解しないままにわざわざ禁止する必要は無いのです。
「どんなに注意しても、人間は間違いを犯す」と言う基本的な事実があります。
printf 内のフォーマットと、可変長の引数の「型」の整合性は、コンパイラではチェックに限界があり、極めて深刻な問題をそのままエラー無くコンパイルする事が可能で、その場合、スタックを破壊する事で、微妙で見つけにくいバグをアプリケーションの動作に取り込んでしまう事があります。
iostream であれば、うっかりミスは、コンパイラがエラーを出しますし、スタックを壊すような危険な事もありません。
それでも、尚、printf の使い易さが忘れられない人には、「boost/format.hpp」を使って下さい。
これなら、ほとんど printf のような感覚で、しかも、安全に運用する事が出来ます。
※フォーマットと、引数の型の誤りには「例外」がスローされます。
文字列から数値、数値から文字列の変換には、「boost/lexical_cast.hpp」
と言うのがあり、非常に便利です、これがあれば、scanf を使う必要もありません。
※変換の失敗には「bad_lexical_cast 例外」がスローされるので、それをキャッチして、変換の失敗に備える必要がある。
これはゲームの開発での話ですが、ほとんど何処の会社でも printf を使う事を禁止しています、それは、どんなに注意しても、ミスを無くす事は出来ない為、見つける事が困難な問題をシステムに混入させてしまう危険を避ける為です。
※通常、リリースビルドでは #define で printf、sprintf などをオーバーロードして、命令を無効にするようなマクロが組まれています。
ただ、組み込みでは、残念な事があります、gcc の stdc++ ライブラリーでは、iostream クラスは他との依存が大きく巨大で、リンクすると、通常数百キロバイトのプログラムメモリーと、数十キロバイトのワークエリアを消費してしまいます、これでは、少ないリソースのマイコンでは物理的に使えません。
そこで、もし必要なら、小規模な iostream クラスを作成する必要があります(これは車輪の再発明ではありません)、ホビーで使うのであっても、あくまでも、安全性と利便性から、printf を使わない決断をすべき事項だと思います。
※以前、C 言語の時代でも、printf が巨大な為、多くの人が、tiny printf のような物を作って使っていました。
※オペレーターや、オブジェクト指向の勉強にもなるので、自分で作ってみると楽しいかもしれません。
一応、私が実装した、クラスを紹介します。
※これは、出力先として、「void sci_put(char);」、「void sci_puts(const char*);」などのシリアルインターフェースを使っています。
namespace utils {
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
/*!
@brief chout クラス
*/
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++//
struct chout {
static const char endl = '\n';
private:
char sup_ch_;
char hex_ch_;
uint8_t len_;
public:
//-----------------------------------------------------------------//
/*!
@brief コンストラクター
@param[in] out 文字出力関数
*/
//-----------------------------------------------------------------//
chout() : sup_ch_('0'), hex_ch_('a'), len_(0) { }
//-----------------------------------------------------------------//
/*!
@brief 16 進表示の英数字を大文字にする
@param[in] cap 「false」を指定すると小文字
*/
//-----------------------------------------------------------------//
void hexa_decimal_capital(bool cap = true) {
if(cap) hex_ch_ = 'A'; else hex_ch_ = 'a';
}
//-----------------------------------------------------------------//
/*!
@brief 表示文字数を指定
@param[in] len 表示文字数
*/
//-----------------------------------------------------------------//
void set_length(uint8_t len) { len_ = len; }
//-----------------------------------------------------------------//
/*!
@brief ゼロサプレス時の文字を指定
@param[in] ch 文字
*/
//-----------------------------------------------------------------//
void suppress_char(char ch) { sup_ch_ = ch; }
//-----------------------------------------------------------------//
/*!
@brief 文字表示
@param[in] ch 文字
*/
//-----------------------------------------------------------------//
void put(char ch) const { sci_putch(ch); }
//-----------------------------------------------------------------//
/*!
@brief 文字列の表示
@param[in] str 文字列
*/
//-----------------------------------------------------------------//
void string(const char* str) const { sci_puts(str); }
//-----------------------------------------------------------------//
/*!
@brief 長さ指定文字列表示
@param[in] str 文字列
@param[in] len 長さ
*/
//-----------------------------------------------------------------//
void len_string(const char* str, uint8_t len) const {
while(len_ > len) {
put(sup_ch_);
++len;
}
string(str);
}
//-----------------------------------------------------------------//
/*!
@brief 16 進数の表示
@param[in] val 値
*/
//-----------------------------------------------------------------//
uint16_t hexa_decimal(uint32_t val) const {
char tmp[8 + 1];
uint16_t pos = sizeof(tmp);
--pos;
tmp[pos] = 0;
do {
--pos;
char n = val & 15;
if(n > 9) tmp[pos] = hex_ch_ - 10 + n;
else tmp[pos] = '0' + n;
val >>= 4;
} while(val != 0) ;
len_string(&tmp[pos], sizeof(tmp) - pos - 1);
return sizeof(tmp) - pos - 1;
}
//-----------------------------------------------------------------//
/*!
@brief 10 進数の表示
@param[in] val 値
@param[in] minus マイナス符号を表示する場合「true」
@return 表示文字数
*/
//-----------------------------------------------------------------//
uint16_t decimal(uint32_t val, bool minus = false) const {
char tmp[11 + 1];
uint16_t pos = sizeof(tmp);
--pos;
tmp[pos] = 0;
do {
--pos;
tmp[pos] = '0' + (val % 10);
val /= 10;
} while(val != 0) ;
if(minus) {
--pos;
tmp[pos] = '-';
}
len_string(&tmp[pos], sizeof(tmp) - pos - 1);
return sizeof(tmp) - pos - 1;
}
//-----------------------------------------------------------------//
/*!
@brief 符号付き 10 進数の表示
@param[in] val 値
@return 表示文字数
*/
//-----------------------------------------------------------------//
uint16_t decimal(int32_t val) const {
bool minus = false;
if(val < 0) {
minus = true;
val = -val;
}
return decimal(static_cast(val), minus);
}
chout& operator << (const uint32_t val) {
decimal(val);
return *this;
}
chout& operator << (const int32_t val) {
decimal(val);
return *this;
}
chout& operator << (const char* str) {
string(str);
return *this;
}
chout& operator << (const char ch) {
put(ch);
return *this;
}
};
}
※ただ、このクラスでは、浮動小数点や、2進数、8進数の表示をサポートしていません、浮動小数点表示が出来ないのは痛いので、改修する予定です。
現在、独自の format クラス一式を実装してあります、GitHUB 参照下さい。