Arduino – ESP32 の関数の参照渡し、ポインタ渡し、String渡しについて考えてみた

ESP32 ( ESP-WROOM-32 )

コーディング規約による参照渡しに const を付けなければいけない理由と、ポインタ渡しについて

(※ここからは私の推測も入っているので、勘違いもあると思います。確実な事はご自分で調べてください)

以上のように、関数の引数で構造体やクラスを使う場合は、メモリの無駄遣いを避けるためにも、’&’ を付けた「参照渡し」にした方が良いです。

その他に、全く同じような動作をする「ポインタ渡し」というものがあります。
では、それを使ってコードを比較してみましょう。

まずは、一旦「参照渡し」のコードに戻りますが、先のコードをもっと簡略化した以下のコードを例とします。

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

class Test {
  public:
    String str1;
    String str2;
};

void setup() {
  Serial.begin(115200);
  Serial.println();

  Test test;

  test.str1 = "abcdefghijklmnopqrstuvwxyz";
  test.str2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

  printTestData( test ); //Testクラスの参照渡し
}

void loop() {

}

void printTestData( Test &t ) { //Testクラスの参照渡し
  Serial.println( t.str1 );
  t.str2 = "12345" + t.str2;
  Serial.println( t.str2 );
}

実行結果

では、次はこれを「ポインタ渡し」の場合のコードに替えてみます。
以下のようになります。

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

class Test {
  public:
    String str1;
    String str2;
};

void setup() {
  Serial.begin(115200);
  Serial.println();

  Test test;

  test.str1 = "abcdefghijklmnopqrstuvwxyz";
  test.str2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

  printTestData( &test ); //Testクラスのポインタ渡し
}

void loop() {

}

void printTestData( Test *t ) { //Testクラスのポインタ渡し
  Serial.println( t->str1 );
  t->str2 = "12345" + t->str2;
  Serial.println( t->str2 );
}

実行結果

出力結果は全く同じ結果で、ここでは表示していませんが、ヒープメモリの消費も同じ結果になります。

以上から、どちらを使っても同じ結果になるので、使いたい方を使えば良いと思ってしまいます。
しかし、実は、プログラム熟練者や職業プログラマの間では明確な区別があるようです。

では、まずソースコードを眺めてみると、Arduino コーディングに慣れている人は「参照渡し」の方が圧倒的に簡単で分かりやすいですね。

逆に、「ポインタ渡し」の方は、難解な記号がかなりあります。
特に、24~26行の ‘->’ という記号は、私はちょっと前までは拒絶反応を示したくらい嫌な記号でした。
これはアロー演算子というものです。
知ってしまえばそれほど難しくないのですが、知るまでは超難解に思っていました。

参照渡しでは
t.str1
という風に、ピリオドを使いますが、ポインタ渡しになると
t->str1
となるだけなんですけどね。

アロー演算子はパッと見た目が矢印のように見えて、tをstr1に代入するようにも見えてしまうところがややこしいのです。
Arduino のようなビギナーが使うプログラミングでは、これは使わない方が無難で、ライブラリとして隠蔽してしまった方が良いと私は思っています。

また、16行目の
printTestData( &test );
のように、関数にクラスを代入するところで ‘&’ が付いているところが問題です。

もし、23~27行目の関数が、ライブラリ作者によって隠蔽されてしまった場合、ビギナーの私たちはいちいちライブラリの関数を見に行って解析したりしませんので、ポインタを知らない人たちは普通に
printTestData( test );
というように ‘&’ を付けないで代入してしまうでしょう。

すると、当然コンパイルエラーになります。
プログラミングビギナーにとっては、この ‘&’ という特殊文字の意味がとっても難解なものだと思います。

でも、熟練者や職業プログラマの方々にとっては、’&’ を付けて関数に渡してあれば、一発で「ポインタ渡し」だと判別できます。

C/C++ で長くコーディングしていると、普通のbyte型やchar型変数でもポインタ渡しをするようになり、ポインタ渡しをした場合は渡した先の関数内で値が変更されることは多々あります。
ということは、熟練者には「ポインタ渡し」は、渡した先の関数内で値が変更される前提のものと定着しているものと思われます。
(あくまで個人的推測です)

私自身も以前、構造体やクラスというものを知らない時に、関数へ渡す複数の引数に戻り値が欲しい時が多々ありました。
ポインタが分かり始めて来た当初は、関数で「ポインタ渡し」を使えば、関数内で複数の変数の値を変更して、戻り値と同じ扱いができるので、とても便利で多用していました。
そのうち、「ポインタ渡し」は渡した先で値が変更されるのは当たり前と思うようになりました。

では、「参照渡し」の場合はどうでしょうか?

printTestData( test );
となっているので、熟練者の方々から見れば、通常の変数代入と同じにパッと見て考えると、test の値は変更できないものと直感的に思うのではないでしょうか。
もちろん、その先の関数の引数に ‘&’ が付いていれば「参照渡し」だと判別できるのですが、プログラムが複雑で膨大になってしまうと、いちいちその先の関数の引数をチェックすることがとっても面倒です。
私も他人の作った複雑で膨大なライブラリ関数を解析するのことは避けたいです。

以上から、おそらく、

「ポインタ渡し」は、渡した先の関数で値が変更される前提
「参照渡し」は、渡した先の関数で値が変更されない前提

という一般的なコーディング概念が定着したのだと考えられます。
(これについてはあくまで個人的推測です。間違えているかもしれませんので、各自調べてみて下さい。)

この記事の一番下のコメント投稿欄で、早速ご意見いただきました。
「ポインタ渡し」と「参照渡し」の使い分けは、NULL ポインタのチェックに関連することが大きいと教えて頂きました。
Kenta IDAさん、ありがとうございました。
m(_ _)m

 

しかし、先のソースコードを見ての通り、クラスや構造体の場合、「参照渡し」でも渡した先の関数内で値の変更は可能ですね。
そうなってくると、いろいろ混同してややこしくなるので、普通の変数の代入と同じように、「参照渡し」でもパッと見た目で渡した先の関数で値が変更されないように、
積極的に const を付けよう!
となったのだと思われます。たぶん・・・。
(あくまで個人的推測です)

const は C++ の修飾子で、これを付けるとその関数内で値の変更ができなくなります。
こんな感じです。

【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

class Test {
  public:
    String str1;
    String str2;
};

void setup() {
  Serial.begin(115200);
  Serial.println();

  Test test;

  test.str1 = "abcdefghijklmnopqrstuvwxyz";
  test.str2 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

  printTestData( test ); //Testクラスの参照渡し

  test.str2 = "12345" + test.str2; //ここでは値の変更は可能
  Serial.println( test.str2 );
}

void loop() {

}

void printTestData( const Test &t ) { //Testクラスの参照渡し
  Serial.println( t.str1 );
  //t.str2 = "12345" + t.str2; //※この関数内では値を変更できない
  Serial.println( t.str2 );
}

28行目は実行してしまうとコンパイルエラーになるのでコメントアウトしています。
26行目で const を付けたことによって、値が変更できなくなったためです。
でも、関数を抜けた後は、const の呪縛は解かれているので、18行目のように普通に値の変更ができます。
実行結果は以下のようになります。

以上より、一般的な C / C++ 言語のコーディング規約から、関数の引数渡しについてまとめると、

「ポインタ渡し」は、渡した先の関数で値が変更される前提
「参照渡し」は、基本的に渡した先の関数で値が変更されないものとして const をつける

ということだと思います。
プログラマの間では一般的なようなので、これに従った方が良いかもしれません。
(あくまで個人的推測)

一番下のコメント投稿欄で、早速ご意見いただきました。
特にそういうことは無いそうです。
「ポインタ渡し」にも const 付ける場合もあるし、「参照渡し」でも const を外すこともあるようです。
Kenta IDA さん、ありがとうございました。
m(_ _)m

 

Google のC++コーディングスタイルガイドに似たようなことが書かれていますので参照してみてください。
以下のリンクは日本語訳です。

https://ttsuki.github.io/styleguide/cppguide.ja.html#Use_of_const

これによると、その値を変更するつもりが無い場合は const を付けるべしと書いてあります。
要するに「定数にすべし」という事ですね。

ただし!!!

Arduino コーディングの場合はちょっと違う概念を持った方が良いかもしれません。
要するに、ビギナーが主な対象になるからです。
熟練プログラマの間の一般的なコーディング規約とは違う概念と思った方が良いかも知れません。
次でその理由を述べてみます。

Arduino プログラミング開発の場合、関数への参照渡しは const を付けなくても良いのかな?

前項では、関数の引数の「参照渡し」は const を付け、渡した先の関数内では値を変更しない方が良く、値を変更したければ「ポインタ渡し」を使うべしと述べました。

でも、Arduino コーディング(プログラミング)の場合、ライブラリを自作して公開する場合、「ポインタ渡し」の関数を使ってしまうと、先にも述べたように ‘&’ という記号がスケッチ上の変数に現れてしまうので、ビギナーにとっては難解なものになってしまいます。

以前にもこのブログでちょっと触れたことがあるかも知れませんが、Arduino コーディング(プログラミング)の場合は、末端のビギナーユーザーの為に、専門用語やポインタを意識しない設計にした方が良いのです。

本家 Arduino のホームページに Arduino API 作成用コーディングスタイルガイドが以下のリンクにあります。
自分でライブラリを作成して、公開する場合にはこれを参考にすると良いと思います。
(英語版なので Google 翻訳して読んでください)

https://www.arduino.cc/en/Reference/APIStyleGuide

これによると、末端のユーザーにわかりやすいように、ポインタを意識しないようにプログラミングして、できるだけ「参照渡し」にした方が良いようなことが書かれています。

と、いうことは・・・ですよ?!

「ポインタ渡し」を使わないなら、「参照渡し」に限定されるわけです。
そうすると、先に述べた一般的コーディングスタイルに乗っ取って const を付けてしまったら値が変更できないので、とても具合が悪いです。

と、いうことを考えると、私なりの結論として、

Arduino IDE 用限定のライブラリを自作して公開するのならば、関数の引数の「参照渡し」で値が変更できるように const を外して使っても良い

と言えそうです。
(あくまで個人的推測です)

こうなると、職業プログラマや熟練プログラマからは Arduino コーディングが嫌われるのは理解できる気がします。
Arduino コーディングでライブラリを公開する方々は、両方の言い分を知っていると良いかも知れませんね。

これについても下のコメント欄でご意見いただきましたので、合わせてご参照ください。

 

コメント

  1. Kenta IDA より:

    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*を受けるオーバーロードを用意しておくと便利かと思います。

    • mgo-tec mgo-tec より:

      Kenta IDA さん

      早速コメントありがとうございます。

      なるほど!
      ポインタの NULL チェックはまったく使ったことがありませんでした。
      参照渡しとポインタ渡しの使い分けは NULL チェックが大きいとは知りませんでした。
      やっぱり独学はダメですね。
      こういうコメントで指摘されないと、自分の想像の域から脱せませんから。
      とても勉強になりました。
      手が空いたら、その辺をもう一度調べ直してみます。
      丁寧なコメント、ありがとうございました。
      m(_ _)m

      • mgo-tec mgo-tec より:

        それと、
        printTestData( “hello”, “world” ); 
        というところは、const char* の別関数を作ってオーバーロードさせるっていうところも、その手があったか!!!
        という感じで目から鱗です。
        すばらしいです。
        教えて頂き、ありがとうございます。
        m(_ _)m

  2. take4 より:

    もしよかったら、Effective C++を一読されたらいかがでしょうか。C++の設計/実装方法に関する定番のようなものが載っています。
    参照やconstを使う理由についても書かれています。

    ただ、本の記述として回りくどい書き方もあったりして、通しで読むには骨が折れます。まず目次に書かれている内容を覚えておき、自分が実装したものに照らし合わせ該当する箇所を読んで適用をしていけば、いいかもしれません。

    • mgo-tec mgo-tec より:

      take4 さん

      コメント頂き、ありがとうございます。

      ネットで調べているだけで、最近本を買っていませんでした。
      やっぱり、正しい知識は書籍の方かも知れませんね。
      私の場合は、職業プログラマのコーディングスタイルではなく、あくまで Arduino コーディングスタイルを目指しているので、その辺の違いももっとよく勉強したいですね。
      そうすると、正規のコーディングスタイルを勉強しておかないと、違いが分からないかも知れませんね。
      なかなか書籍を見る暇がありませんが、購入を検討してみたいと思います。
      情報ありがとうございました。
      m(_ _)m

  3. たらんてら より:

    ポインタと参照の違いについて。NULLの許容云々という面もありますが、一番の違いは「ポインタは動かせる、参照は動かせない」ということです。
    (文字列へのポインタを++してみるとわかりやすいでしょう)

    ポインタは仕組み上原始的で、使い方次第でとても便利ですが、一方でバグの温床にもなり易く、最悪プログラムが暴走したりします。

    参照は、ポインタの便利な機能の一部分を、安全に使うために後から考え出された仕組みです。似ているのは当然で、違いについて考えてしまうのもわかりますが、ポインタでなければできない場合以外は参照を使うべきです。

    • mgo-tec mgo-tec より:

      たらんてら さん

      記事をごらんいただき、ありがとうございます。
      只今、当ブログの不具合が生じて、メンテナンスに追われております。
      ですから、コメントが表示されたり、表示されなかったり、過去記事が消えたりと、いろいろ修正中です。
      お返事が遅くなり、申し訳ございませんでした。

      ポインタと参照の歴史は知りませんでした。
      私はネットで調べた情報しか知らないもので、まだまだ未熟でした。
      やはり、熟練者からそういう情報を頂くと、とてもよく分かりますね。
      奥が深いです。
      情報を下さってありがとうございます。
      m(_ _)m

  4. chimu2 より:

    いやぁ、コード始めたばかりって、こういう細かい葛藤ありますよねぇ。
    ただ、なぜそのように書くのかっていう理由って、あまり記述されているものってなかなか見つからないし、時間もなくて適当にサンプルのマネしちゃうので、技術力があがらないのだと思います。
    すごく参考になりましたし、こういう発信をしてもらえると初心者には特に理解の助けになると思います。頑張ってください!

    • mgo-tec mgo-tec より:

      chimu2 さん

      記事をご覧いただき、ありがとうございます。
      そう思っていただけてうれしいですね。

      この記事は2019年1月頃に書いたもので、最近はC/C++コーディングから離れていたので、久々に読み返しちゃいました。
      そうだったっけ? と我ながら思ってしまいました。
      最近はなかなか時間が無いのですが、またコーディング漬けな時間が欲しいと思う次第です。
      こういうことを疑問に思う時間は貴重ですね。

タイトルとURLをコピーしました