🕳️

A way to format string on Arduino(Wio Terminal)

に公開

ArduinoではString::formatが使えない

大抵のプログラミング言語では、フォーマット文字列と変数を用いて、動的に文字列を生成することができます。

C++の場合

関数std::format()を使います。

C++の場合
#include <iostream>
#include <format>

int main() {
    int iValue = 42;
    float fValue = 3.14159f;
    double dValue = 2.718281828459;

    std::string formatted = std::format("Integer: {}, Float: {}, Double: {:.4f}", iValue, fValue, dValue);

    std::cout << formatted << std::endl;
    std::cout << __cplusplus << std::endl; // C++のバージョンが表示される
    return 0;
}

ただしstd::formatC++20の機能であるため、それよりも古い方式でコンパイルするとerror: 'format' is not a member of 'std'というエラーになります。その際は、次のようにC++20を指定する必要があります。

https://tech.garilog.com/gcc-cpp-version/

コンパイル
g++ -std=c++20 .\main.cpp # ここではmain.cppというファイル名

実行結果はこの通りです。

実行結果
Integer: 42, Float: 3.14159, Double: 2.7183
202002
Pythonの場合

文字列の頭にfを付け、f'~'とするだけで可能です。

Pythonの場合
>>> iValue: int = 42
>>> fValue: float = 2.718281828459
>>> dic: dict = {'date': '2025-06-01', 'title': 'dictionary', 'height': 24}
>>> f'int: {iValue}, float: {fValue}, dict: {dic}'
"int: 42, float: 2.718281828459, dict: {'date': '2025-06-01', 'title': 'dictionary', 'height': 24}"
>>> f'{fValue:.2f}'
'2.72'
>>> f'{fValue:.2}'
'2.7'
>>> f'{fValue:.9f}'
'2.718281828'
>>> f'{fValue:.9}'
'2.71828183'

関数format()もありますが、やや癖があります。

>>> "{}".format(2)
'2'
Rustの場合

マクロformat!()を使います。

Rustの場合
fn main() {
    let u64_value: u64 = 42;
    let f64_value: f64 = 2.718281828459;
    let ai64_array: [i64; 5] = [1, 3, 5, 7, 9];

    // 配列のformatには{:?}を使う
    let s_format = format!("uint: {}, float: {}, array: {:?}", u64_value, f64_value, ai64_array);

    println!("{s_format}");
}
実行結果
uint: 42, float: 2.718281828459, array: [1, 3, 5, 7, 9]

一方現状のArduinoでは、formatに近しい機能が使えないようです。従って、Cのように配列を使う方法を取らなければなりません。危険とされるsprintfではなく、比較的安全とされるsnprintfを使いましょう。

Cの場合
#include <stdio.h>
            
int main() {
    const int iBufferSize = 64;
    char acBuffer[iBufferSize];

    int iValue = 42;
    float fValue = 3.14159f;
    double dValue = 2.718281828459;

    snprintf(acBuffer, iBufferSize, "Integer: %d, Float: %f, Double: %.4f", iValue, fValue, dValue);

    printf("%s", acBuffer);

    return 0;
}
実行結果
Integer: 42, Float: 3.141590, Double: 2.7183

なおconst int iBufferSize = 16;と変更すると、表示がInteger: 42, Flで途切れます。16バイトを超える分が除かれるためです。sprintfで同じことを真似するとエラーになります。

AIに実装させた

文字列を扱うたびに専用の配列とバッファーサイズを管理するのが忌まわしくて仕方ないので、AIformatを実装させました。

AIによる実装例
#include <type_traits> // std::is_same

// ベースケース: 引数がない場合
String format_impl(const String& format_str) {
    return format_str;
}

// 浮動小数点数を指定した小数点以下桁数で文字列化する関数
String floatToString(float value, int digits) {
    char buf[32];
    dtostrf(value, 0, digits, buf); // dtostrf(値, 全体幅, 小数点以下桁数, バッファ)
    return String(buf);
}

// 再帰的なフォーマット関数
template<typename T, typename... Args>
String format_impl(const String& format_str, T first_arg, Args... rest_args) {
    // フォーマット文字列内の浮動小数点数のプレースホルダー "{:.nf}" を探す
    int float_placeholder_pos = format_str.indexOf("{:."); // 例: {:.4f}
    // 浮動小数点数のプレースホルダーが見つかった場合
    if (float_placeholder_pos != -1) {
        int end = format_str.indexOf("f}", float_placeholder_pos);
        if (end != -1) {
            String before = format_str.substring(0, float_placeholder_pos);
            String after = format_str.substring(end + 2);

            // first_argがfloat/double型の場合のみfloatToStringを使う
            if constexpr (std::is_same<T, float>::value || std::is_same<T, double>::value) {
                int digits = format_str.substring(float_placeholder_pos + 3, end).toInt();
                return before + floatToString(first_arg, digits) + format_impl(after, rest_args...);
            } else {
                return before + String(first_arg) + format_impl(after, rest_args...);
            }
        }
    }

    // 通常のプレースホルダー "{}" を探す
    int placeholder_pos = format_str.indexOf("{}");

    // プレースホルダーが見つからない場合
    if (placeholder_pos == -1) {
        return format_str;
    }

    String before_placeholder = format_str.substring(0, placeholder_pos);
    String after_placeholder = format_str.substring(placeholder_pos + 2); // "{}" は2文字

    return before_placeholder + String(first_arg) + format_impl(after_placeholder, rest_args...);
}

// ユーザー向けのエントリポイント
template<typename... Args>
String format(const char* fmt_cstr, Args... args) {
    return format_impl(String(fmt_cstr), args...);
}


void setup() {
    Serial.begin(115200);
    while (!Serial);

    int p1 = 10;
    float p2 = 1.5;

    String s1 = format("param1: {}, param2: {}", p1, p2);
    Serial.println(s1);

    String s2 = format("Hello, {}!", "World");
    Serial.println(s2);

    String s3 = format("Number: {}", 42);
    Serial.println(s3);

    String s4 = format("Number: {}", 45.6);
    Serial.println(s4);

    String s5 = format("Number: {:.1f}", 45.6);
    Serial.println(s5);

    String s6 = format("Napier: {:.5f}", 2.71828);
    Serial.println(s6);

    // 引数が足りない場合
    String sShortage = format("One param: {}, two param: {}", 100);
    Serial.println(sShortage);

    // 引数が多い場合
    String sExcess = format("Just one: {}", 1, 2);
    Serial.println(sExcess);
}

void loop() {}

シリアルモニターへの出力結果はこの通りです。

serial monitor
param1: 10, param2: 1.50
Hello, World!
Number: 42
Number: 45.60
Number: 45.6
Napier: 2.71828
One param: 100, two param: {}
Just one: 1

本題:実はString::formatが使える

表題にある通り「ArduinoではString::formatが使えない」と申しますが、「実はString::formatを使えます」。このように矛盾した情報が混在するのは、String定義が異なるためです。

Arduino(AVR)の場合

俗にArduino言語などと称されるように、Arduinoには独自の実装が存在します。本記事の主題であるStringクラスは、当然Cにあるはずもなく、C++std::stringとも異なります。またシリアルモニターで頻繁に使うSerialは、厳密にはSerial_クラスから生成されたオブジェクト(インスタンス)です。

https://github.com/arduino/ArduinoCore-avr/tree/master

Stringクラスの定義はこのように存在します。

https://github.com/arduino/ArduinoCore-avr/blob/master/cores/arduino/WString.h#L45-L211

これを見ると、formatという定義は見当たらないことが確認できます。従って、「ArduinoではString::formatが使えない」のです。

Seeeduino(SAMD)の場合

一方、Wio Terminalを始めとするSeeed製ボードではこちらを使います。

https://github.com/Seeed-Studio/ArduinoCore-samd/tree/master

Stringクラスの定義はこのようにあります。

https://github.com/Seeed-Studio/ArduinoCore-samd/blob/master/cores/arduino/WString.h#L44-L214

これを見ると、String::formatの定義が認められます。

https://github.com/Seeed-Studio/ArduinoCore-samd/blob/master/cores/arduino/WString.h#L76

この実装はソースファイルに分かれています。

https://github.com/Seeed-Studio/ArduinoCore-samd/blob/master/cores/arduino/WString.cpp#L131-L141

従って「Seeeduino(SAMD)の場合、String::formatが使える」のです。

void setup() {
    Serial.begin(115200);
    while (!Serial);

    Serial.println( String::format("Hello, %s!", "World") );

    Serial.println( String::format("Number: %d", 42) );

    Serial.println( String::format("Number: %f", 45.6) );

    Serial.println( String::format("Napier: %.5f", 2.71828182846) );

    Serial.println( String::format("One param: %d, two param: %d", 100) );

    Serial.println( String::format("Just one: %d", 1, 2) );
}

void loop() {}

serial monitor
Hello, World!
Number: 42
Number: 45.600000
Napier: 2.71828
One param: 100, two param: 536871044
Just one: 1

但し、フォーマット指定子に対して引数が足らない場合、コンパイルエラーなどにはなりません。何らかの予期せぬ値が出てくるようです。危険ですね。

まとめ

String::formatの実装を移植すれば、Arduino UNOなどでも使えるようになる可能性があります。しかし、フォーマット指定子自体の煩わしさに加え、引数不足に於ける危険は承知の上で使う必要があります。

或いはAIに実装させたようなものの方が、引数が不足する場合の定義もされており安全かもしれません。また、フォーマット指定子の使い分け(整数は%d、小数は%f、文字列は%sなど)が必要なく、{}で統一されている点も親切です。

String::format実装移植 AI実装
プレースホルダー 適切なフォーマット指定子 一律{}
引数不足時 未定義? {}がそのまま残る
実装コード 10行未満 50行程度+<type_traits>

Discussion