LoginSignup
28
27

More than 1 year has passed since last update.

組み込みでも使える、printf に代わる C++ format クラス

Last updated at Posted at 2019-05-03

はじめに

未だに多くの人が、printf を使い続けています。
基本的に、可変引数の場合、パラメーターの受け渡しをスタックで行う為、printf 内のフォームと、不整合のあるパラメーターを指定すると、最悪の場合プログラムがクラッシュします。
コンパイラは、printf のフォームと、引数の不整合が無いかを検査しますが、完全に検査する事は出来ません。

これらの事から、多くの開発現場では、printf の利用を禁止するルールが運用されています。

#include <stdio.h>
#define printf(fmt, ...)

※「printf」を消す。

C++ では、printf に代わる機能として、iostream クラスがあり、文字列の表示を行う事が出来ます。
しかし、iostream クラスを使って文字列を扱った場合、printf にあるようなコンビニエンス性が無く、慣れの問題もあり敬遠されがちだと思えます。
boost では、これらの事情を考慮して、printf に限りなく近く使えて、「安全」 な boost::format クラスが実装されています。
※ boost::format クラスでは、最終的な文字出力は、iostream を使っています。

組み込み開発では、別の問題があります。
通常、組み込みマイコンのような小規模システムでは、iostream 関連(std ライブラリ)を使う事で多くのメモリを消費してしまい、現実的に使う事が困難です。

iostream におけるメモリ消費

以下は、RXマイコン、g++ による例です。

    std::cout << "Hello!" << std::endl;

   text    data     bss     dec     hex filename
 506976   47556    8796  563328   89880 test_test.elf

---
    utils::format("Hello!\n");

   text    data     bss     dec     hex filename
   5120      48    1860    7028    1b74 test_test.elf

上記のように「iostream」を使った場合と、自前「format」を使った場合の実行バイナリの違いは明らかです。


printf が文字出力する仕組み

まず、システムにおいて、printf が文字を出力する仕組みを「おさらい」します。

printf は、文字出力として、putchar 関数を呼び出しています。
putchar 関数は、標準出力「stdout」に対して、ファイル書き込みに相当する関数を呼び出します。
従って、write 関数をオーバーライドすれば、printf で文字出力した先を自由に操作する事が出来ます。

write 関数を呼び出す際、ファイルハンドルとして、「stdout」を使います。
※通常、「stdout」は、「1」番が使われます、「stderr」は「2」番。

write 関数など、ファイル操作関係の関数は、特殊な属性で、gcc では以下のように定義されています。

ssize_t write(int fd, const void* src, size_t len)  __attribute__((weak));

「weak」は、リンクの際、実態があれば、その関数がリンクされます。

この仕様の為、アプリ側で、write を実装して、たとえば、シリアル出力に送れば、printf した場合に、最終的に文字列はシリアル出力されます。

_READ_WRITE_RETURN_TYPE write(int file, const void *ptr, size_t len)
{
    if(ptr == NULL) return 0;

    _READ_WRITE_RETURN_TYPE l = -1;
    if(file >= 0 && file <= 2) {
        if(file == 1 || file == 2) {
            const char *p = ptr;
            for(int i = 0; i < len; ++i) {
                char ch = *p++;
                sci_putch(ch);
            }
            l = len;
            errno = 0;
        }
    }

...

    return l;
}

自前の「format」クラスの使い方

C言語の時代、組み込み開発では、libc の printf が巨大な為、自前の printf を実装していました。
これは、float を使わないとか、カスタマイズでき、適度な規模であった事などが理由と思います。
それに習って、boost::format クラスに代わる、format クラスを実装しています。
※車輪の再発明となっていますが、カスタマイズする事が出来る為、多少の意味はあると考えます。

例えば、以下のように使えます。

    int value = 123;
    utils::format("Value=%d\n") % value;

C++ では、オペレーター機能があり、この場合、算術の「%」を別の機能として利用する事が出来ます。
可変引数を使っていないので、不整合のある指定を行ったとしても、スタックが壊れる事もなく安全です。
boost::format では、不整合のある引数を指定した場合、「例外」がスローされますが、組み込みでの利便性を考えて、内部エラーコードをサービスします。

    float a = 1.0f;
    auto err = (utils::format("Fail int: %d\n") % a).get_error();
    utils::format("Error: %d\n") % static_cast<int>(err);

冗長ですが、エラーコードを取得して検査する事も出来ます。

    enum class error : uint8_t {
        none,       ///< エラー無し
        null,       ///< 無効なポインター
        unknown,    ///< 不明な「型」
        different,  ///< 異なる「型」
    };

通常、stdout に出力されますが、文字列を作りたい場合は、以下のようにします。

    using namespace utils;
    float x = 1.0f;
    float y = 2.0f;
    float z = 3.0f;
    char tmp[512];
    sformat("Value: %f, %f, %f\n", tmp, sizeof(tmp)) % x % y % z;
    sformat("Value: %f, %f, %f\n", tmp, sizeof(tmp), true) % y % z % x;
    sformat("Value: %f, %f, %f\n", tmp, sizeof(tmp), true) % z % x % y;
    int size = sformat::chaout().size();
    format("(%d)\n%s") % size % tmp;

Value: 1.000000, 2.000000, 3.000000
Value: 2.000000, 3.000000, 1.000000
Value: 3.000000, 1.000000, 2.000000

※上記の例では、文字バッファに「追記」もしています。(「true」)


組み込みマイコンでは、A/D変換された整数値など、固定小数点で表示する場合が多いので、それをサポートしています。

{  // 固定小数点
    uint16_t val = (1 << 10) + 500;
    format("Fixed point: %4.3:10y\n") % val;
}

Fixed point: 1.488

上記の例では、小数点以下10ビット、表示は全4桁、小数点以下3桁の場合です。


現在、制限事項として、倍精度浮動小数点の表示を完全にサポートしていません。
※printf では、内部の扱いは「double」が基準で、「float」は「double」にキャストされています。
また、浮動小数点表示も、printf の場合と異なるかもしれません、これは十分なテストがされていない為です。
また、IEEE754 浮動小数点フォーマットのデコードを独自に行っている事も理由です、それでも、通常の表示は問題無いと思います。
※最初、printf の実装を参考にしようと思いましたが、あまりに複雑なので、再利用する事が難しいと判断した為です。
※内部処理では、float の演算を一切使用していません、全て整数だけで処理しています、これは、float 関連をエミュレーションで処理するマイコンではライブラリをリンクする必要性があり、実行バイナリーが巨大になる為です。
「format.hpp」で全て簡潔しています、RXマイコン以外のシステムでも、「format.hpp」をインクルードするだけで使えると思います。
浮動小数点を必要としない場合は、「format.hpp」をインクルードする前に、以下のようにすれば、多少メモリを節約する事が出来ます。
※これはリソースが少ない 8/16 ビットマイコンには有用です。
※リソースの節約に主眼が置かれている為、スピードはあまり速くありません。

#define NO_FLOAT_FORM
#include "format.hpp"

カスタマイズ

8/16 ビットマイコンなどリソースの限られたプロジェクトで使う場合、カスタマイズする事が出来ます。
以下の定義をする事で、使わない機能を追い出し、コード量を最適化できます。

// float を無効にする場合(8ビット系マイコンでのメモリ節約用)
// #define NO_FLOAT_FORM

// 2進表示をサポートしない場合(メモリの節約)
// #define NO_BIN_FORM

// 8進表示をサポートしない場合(メモリの節約)
// #define NO_OCTAL_FORM

標準出力のカスタマイズ

通常、標準出力は、stdout ハンドルを使い、write 関数を呼び出します。
処理系によっては、putchar 関数を使う事を強要される場合があり、その場合、以下の定義を使います。

  • コメントアウトを外して下さい。
// 最終的な出力として putchar を使う場合有効にする(通常は write [stdout] 関数)
#define USE_PUTCHAR

プロジェクト(全体テスト)

  • format クラスは、全体テストと共に提供されます。
  • mingw64 環境で、clang++ を使ってコンパイルされます。
make

で全体テストがコンパイルされます。

make run
  • 全体テストが走り、全てのテストが通過すると、正常終了となります。
  • テストに失敗すると、-1 を返します。

個別のテスト

  • 個別のテストだけ動かす事が出来ます。
  • テストは整数で指示出来ます。

例: test-1, test-5 を動かす場合

./test_format_class -1 -5

変換時間表示(目安)

  • 文字出力として「putchar」を使った場合。
 % time ./test_format_class.exe -boost > list
real    0m2.043s
user    0m0.000s
sys     0m0.015s

 % time ./test_format_class.exe -printf > list
real    0m0.695s
user    0m0.015s
sys     0m0.000s

 % time ./test_format_class.exe -format > list
real    0m0.501s
user    0m0.000s
sys     0m0.015s

ソースコード

https://github.com/hirakuni45/format_class
にあります。


Copyright (c) 2020 2022, Hiramatsu Kunihito
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

  • Redistributions of source code must retain the above copyright notice,
    this list of conditions and the following disclaimer.
  • Redistributions in binary form must reproduce the above copyright notice,
    this list of conditions and the following disclaimer in the documentation
    and/or other materials provided with the distribution.
  • Neither the name of the nor the names of its contributors
    may be used to endorse or promote products derived from this software
    without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

28
27
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
27