Arduino の String 型もクラスなので、関数の引数にする場合は参照渡しにすべし
Arduino でとっても良く使う String 型があります。
これは C++ のクラスです。
これ、熟練者の方々には当たり前のことだと思いますが、私はつい最近まで気が付かずに、関数の引数を渡していました。
自分でライブラリのクラスを作成していて、それを引数にして関数に渡す場合には、メモリを意識して「参照渡し」にしているのに、なぜか String 型は全くスルーしていて、普通の変数のように「値渡し」にして、コピーコンストラクタを作成してしまっていました。
メモリの無駄遣いですね。
既に公開してしまったライブラリは、今更変更できないでこのまま残しておきます。
今後公開するライブラリは意識して直していこうと思っています。
String 変数は、Arduino コーディングの場合、char 型文字列よりも手軽でとっても便利なので、よく使います。
char 型のように事前に配列の要素数を決めなくて良く、好きな長さの文字列を格納することができますし。
私の様なアマチュアにとって、電子工作や自作 IoT には欠かせません。
Arduino IDE 用の String クラスの詳しい使い方は、私もよく利用させて頂いている以下のサイトに詳しく書いてあります。
では、先に述べたように、String クラスの「値渡し」と「参照渡し」とで、メモリの使われ方に違いがあるかを比較してみたいと思います。
まず、以下のような簡単なコードを例とします。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)
void setup() { Serial.begin(115200); Serial.println(); String str1, str2; str1 = "abcdefghijklmnopqrstuvwxyz"; str2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; printTestData( str1, str2 ); } void loop() { String str1, str2; str1 = "abcdefghijklmnopqrstuvwxyz"; str2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; printTestData( str1, str2 ); delay(20000); } void printTestData( String &s1, String &s2 ) { Serial.println( s1 ); Serial.println( s2 ); }
まずは、String クラスを「値渡し」にした場合、要所要所にヒープメモリの空きサイズをシリアルモニタに出力させるスケッチは以下のようになります。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)
void setup() { Serial.begin(115200); Serial.println(); Serial.print( "Free Heap Size(setup0) = " ); Serial.println( esp_get_free_heap_size() ); String str1, str2; Serial.print( "Free Heap Size(setup1) = " ); Serial.println( esp_get_free_heap_size() ); str1 = "abcdefghijklmnopqrstuvwxyz"; str2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; Serial.print( "Free Heap Size(setup2) = " ); Serial.println( esp_get_free_heap_size() ); printTestData( str1, str2 ); //値渡し Serial.print( "Free Heap Size(setup3) = " ); Serial.println( esp_get_free_heap_size() ); } void loop() { Serial.println( "---------------------" ); Serial.print( "Free Heap Size(main loop0) = " ); Serial.println( esp_get_free_heap_size() ); String str1, str2; Serial.print( "Free Heap Size(main loop1) = " ); Serial.println( esp_get_free_heap_size() ); str1 = "abcdefghijklmnopqrstuvwxyz"; str2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; Serial.print( "Free Heap Size(main loop2) = " ); Serial.println( esp_get_free_heap_size() ); printTestData( str1, str2 ); //値渡し Serial.print( "Free Heap Size(main loop3) = " ); Serial.println( esp_get_free_heap_size() ); delay(20000); } void printTestData( String s1, String s2 ) { //値渡し Serial.print( "Free Heap Size(Function) = " ); Serial.println( esp_get_free_heap_size() ); Serial.println( s1 ); Serial.println( s2 ); }
実行結果
どうですか?
先に Test クラスを作成したスケッチと同じ結果になり、メモリの減り方も全く同じ結果になりました。
要するに関数の引数で String 型で渡す時、「値渡し」はメモリを無駄に消費しています。
では、今度は「参照渡し」を見てみたいと思います。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)
void setup() { Serial.begin(115200); Serial.println(); Serial.print( "Free Heap Size(setup0) = " ); Serial.println( esp_get_free_heap_size() ); String str1, str2; Serial.print( "Free Heap Size(setup1) = " ); Serial.println( esp_get_free_heap_size() ); str1 = "abcdefghijklmnopqrstuvwxyz"; str2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; Serial.print( "Free Heap Size(setup2) = " ); Serial.println( esp_get_free_heap_size() ); printTestData( str1, str2 ); //参照渡し Serial.print( "Free Heap Size(setup3) = " ); Serial.println( esp_get_free_heap_size() ); } void loop() { Serial.println( "---------------------" ); Serial.print( "Free Heap Size(main loop0) = " ); Serial.println( esp_get_free_heap_size() ); String str1, str2; Serial.print( "Free Heap Size(main loop1) = " ); Serial.println( esp_get_free_heap_size() ); str1 = "abcdefghijklmnopqrstuvwxyz"; str2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; Serial.print( "Free Heap Size(main loop2) = " ); Serial.println( esp_get_free_heap_size() ); printTestData( str1, str2 ); //参照渡し Serial.print( "Free Heap Size(main loop3) = " ); Serial.println( esp_get_free_heap_size() ); delay(20000); } void printTestData( String &s1, String &s2 ) { //参照渡し Serial.print( "Free Heap Size(Function) = " ); Serial.println( esp_get_free_heap_size() ); Serial.println( s1 ); Serial.println( s2 ); }
実行結果
初回のメモリサイズはちょっと不思議な数値ですが、2回目以降は printTestData関数に入る前と入った後でヒープメモリサイズは変わりありません。
以上から、私の結論として
関数の String 型引数は「参照渡し」にすべし!
そして、Arduino ライブラリの関数の場合には const を外しても良い!
ということです。
(あくまで個人的推測)
ただし、以下のように、関数に文字列を直に渡したい場合、
printTestData( “hello”, “world” );
これは「参照渡し」ではエラーになりますのでご注意ください。
こういう用途でも使いたいならば、「値渡し」しかありません。
Arduino 関数ではよく見かけますが・・・。
コメント投稿欄でご意見をいただいたので参照してください。
まとめ
いろいろごたごたと書きましたが、私なりに解釈したことをまとめるとこんな感じです。
● 関数に沢山の引数がある場合は、構造体やクラスにするとメチャ便利。
● 関数の引数に構造体やクラスを使う場合は、メモリ節約のために「参照渡し」か「ポインタ渡し」にすべし。
● Arduino コーディングの場合には「ポインタ渡し」は極力避け、「参照渡し」にする。
● 「参照渡し」の場合、一般的には const を付けるが、Arduino コーディングの場合は const を外しても良い。
頭では分かっているのですが、こうやってブログ記事にするとメチャメチャ長くなってしまいました。
逆に自分自身が勉強になりますね。
実は、コメント投稿欄でご意見いただき、以上の私の推測は必ずしもそうとは限らないということを教えて頂きました。
やっぱり独学だと推測の域を出ないので、改めてこういうご意見をいただくと、とても勉強になります。
叩かれるのを恐れて記事を書かないよりも、いろいろ叩かれてもこういう記事を発信することは、意味のあることだなと思いました。
ということで、自分自身のライブラリをこれから見直していきたいと思います。
私は基本的に素人なので、間違えているかもしれません。
何かお気づきの点がありましたら、コメント投稿等でご連絡いただけると助かります。
ということで、今回はここまでです。
では、皆さまにとっても自分にとっても良い年でありますように・・・。
コメント
Arduino言語にしろC++にしろ、ポインタの場合と違って参照渡しの場合はconstつけた方がいいとかは無いと思います。
constつける理由はもっと直接的な理由で、関数内で引数の内容を変更しないことを表すためです。
ですので、ポインタの場合でも関数内で変更しないのであればconstつけますし、参照の場合でも関数内で変更するのであればconstつけないです。
ポインタと参照の使い分けは、NULL (nullptr) を許容するかどうかが大きいと思います。参照は基本的にNULL にすることができません。一方、ポインタはNULL にすることができます。例えば、ポインタの場合は呼び出し時に引数にNULL を指定することによって「引数を指定しない」ことを表すことができます。
一方、必ず指定しなければいけない引数にをポインタ型にすると、関数の内部でNULLチェックを行う必要がありますが、参照型にしておけば(無理やり入れない限りは)NULLにできませんので安全です。
> ただし、以下のように、関数に文字列を直に渡したい場合、
> printTestData( “hello”, “world” );
printTestData(String(“hello”), String(“world”));
とすれば一応渡せますが、Stringの内部バッファのヒープからの確保と解放が2回分行われるので効率が悪いです。こういう場合、可能であればconst char*を受けるオーバーロードを用意しておくと便利かと思います。
Kenta IDA さん
早速コメントありがとうございます。
なるほど!
ポインタの NULL チェックはまったく使ったことがありませんでした。
参照渡しとポインタ渡しの使い分けは NULL チェックが大きいとは知りませんでした。
やっぱり独学はダメですね。
こういうコメントで指摘されないと、自分の想像の域から脱せませんから。
とても勉強になりました。
手が空いたら、その辺をもう一度調べ直してみます。
丁寧なコメント、ありがとうございました。
m(_ _)m
それと、
printTestData( “hello”, “world” );
というところは、const char* の別関数を作ってオーバーロードさせるっていうところも、その手があったか!!!
という感じで目から鱗です。
すばらしいです。
教えて頂き、ありがとうございます。
m(_ _)m
もしよかったら、Effective C++を一読されたらいかがでしょうか。C++の設計/実装方法に関する定番のようなものが載っています。
参照やconstを使う理由についても書かれています。
ただ、本の記述として回りくどい書き方もあったりして、通しで読むには骨が折れます。まず目次に書かれている内容を覚えておき、自分が実装したものに照らし合わせ該当する箇所を読んで適用をしていけば、いいかもしれません。
take4 さん
コメント頂き、ありがとうございます。
ネットで調べているだけで、最近本を買っていませんでした。
やっぱり、正しい知識は書籍の方かも知れませんね。
私の場合は、職業プログラマのコーディングスタイルではなく、あくまで Arduino コーディングスタイルを目指しているので、その辺の違いももっとよく勉強したいですね。
そうすると、正規のコーディングスタイルを勉強しておかないと、違いが分からないかも知れませんね。
なかなか書籍を見る暇がありませんが、購入を検討してみたいと思います。
情報ありがとうございました。
m(_ _)m
ポインタと参照の違いについて。NULLの許容云々という面もありますが、一番の違いは「ポインタは動かせる、参照は動かせない」ということです。
(文字列へのポインタを++してみるとわかりやすいでしょう)
ポインタは仕組み上原始的で、使い方次第でとても便利ですが、一方でバグの温床にもなり易く、最悪プログラムが暴走したりします。
参照は、ポインタの便利な機能の一部分を、安全に使うために後から考え出された仕組みです。似ているのは当然で、違いについて考えてしまうのもわかりますが、ポインタでなければできない場合以外は参照を使うべきです。
たらんてら さん
記事をごらんいただき、ありがとうございます。
只今、当ブログの不具合が生じて、メンテナンスに追われております。
ですから、コメントが表示されたり、表示されなかったり、過去記事が消えたりと、いろいろ修正中です。
お返事が遅くなり、申し訳ございませんでした。
ポインタと参照の歴史は知りませんでした。
私はネットで調べた情報しか知らないもので、まだまだ未熟でした。
やはり、熟練者からそういう情報を頂くと、とてもよく分かりますね。
奥が深いです。
情報を下さってありがとうございます。
m(_ _)m
いやぁ、コード始めたばかりって、こういう細かい葛藤ありますよねぇ。
ただ、なぜそのように書くのかっていう理由って、あまり記述されているものってなかなか見つからないし、時間もなくて適当にサンプルのマネしちゃうので、技術力があがらないのだと思います。
すごく参考になりましたし、こういう発信をしてもらえると初心者には特に理解の助けになると思います。頑張ってください!
chimu2 さん
記事をご覧いただき、ありがとうございます。
そう思っていただけてうれしいですね。
この記事は2019年1月頃に書いたもので、最近はC/C++コーディングから離れていたので、久々に読み返しちゃいました。
そうだったっけ? と我ながら思ってしまいました。
最近はなかなか時間が無いのですが、またコーディング漬けな時間が欲しいと思う次第です。
こういうことを疑問に思う時間は貴重ですね。
今まで読んだ構造体・クラス・ポインタの記事の中で一番わかった気になれました!
MILLさん
記事をご覧いただき、ありがとうございます。
しばらくこのブログ記事からは離れていたのですが、久々にこういうコメント頂くとめちゃめちゃウレシイで~す!!!
。