しなぷすのハード製作記

C++でのローカル関数の実現法

ツイート
シェア
このエントリーをはてなブックマークに追加
フォロー
2016年06月21日 公開。
2016年06月23日 リスト2の無名名前空間の例を追加。
2016年06月29日 リスト12の構造体のメンバ変数を使ってauto変数をローカル関数と共有する例を追加。

このサイトでは、普段はハードウェアの話がメインになるのですが、今回は純粋にソフトウェアの話です。

C++では、関数を宣言すると、そのスコープはプロジェクト全体に広がり、どこからでも見えてしまいます。一方で、特定の関数からしか呼ばれない関数を、他の関数から隠蔽したい事は良くあります。

この記事では、特定の関数からしか見えない、ローカルスコープの関数(以後、ローカル関数と呼ぶ)をC++で疑似的に実現する方法を説明します。

目次

1. C++の関数のスコープ … 1ページ
1-1. 普通の関数のスコープ … 1ページ
1-2. 無名名前空間内の関数、static宣言された関数のスコープ … 1ページ
2. ローカルクラスを使ったローカル関数の実現法 … 1ページ
3. ローカルな関数オブジェクトを使って記述をきれいにする … 1ページ
4. ローカルな関数オブジェクトを使って、再帰呼び出しのあるローカル関数を実現する … 1ページ
5. ローカル関数内のローカル関数 … 1ページ
6. ローカル関数外の変数へのアクセス … 1ページ
6-1. グローバル変数へのアクセスは可能 … 1ページ
6-2. 関数内のローカル変数へはstatic変数のみアクセス可能 … 1ページ
7. その他の制限 … 1ページ
8. Arduino IDEでの動作確認 … 1ページ
9. 最後に … 1ページ

1.C++の関数のスコープ

本題に入る前に、C++で関数を宣言する場合に、関数がどれだけの広さのスコープを持つかをまず確認しておきましょう。ここでいう関数は、クラスのメンバ関数ではなく、どのクラスにも、どの名前空間にも所属しない関数の事を指します。

1-1.普通の関数のスコープ

例えば、リスト1に示す様な関数globalがあるとします。

リスト1、どこからでも見える関数
int global(int n)
{
  return 2*n;
}

この関数のスコープは、同一プロジェクト全体に広がります。global関数を定義したファイルからも呼び出す事ができますが、同一プロジェクトの他のファイルからかも呼び出す事ができます。

もしリスト1の様な形で関数を宣言すると、複数のソースリストからなるプロジェクトの開発をしている場合、各ファイルで同じ名前の関数ができないように、関数の命名に気を付けないといけません。うっかり同じ名前の関数を同一プロジェクト内で複数作ると、リンク時にエラーになります。これは、複数の人が共同して開発を行う場合に、特に深刻な問題になります。

1-2.無名名前空間内の関数、static宣言された関数のスコープ

他のファイルから関数を隠蔽したい場合は、リスト2の様に、無名名前空間を使うか、リスト3の様にstatic修飾子を付けて関数を定義します。

注:関数をstatic宣言してスコープをファイル内に限定する手法は、C言語との互換性のために用意されている機能で、C++では利用を推奨されていません。

リスト2、同一ファイル内からのみ見える関数(無名名前空間内の関数)
namespace {
  int file_local(int n)
  {
    return 2*n;
  }
}
リスト3、同一ファイル内からのみ見える関数(static宣言された関数)
static int file_local(int n)
{
  return 2*n;
}

これらの様にしておけば、同一プロジェクトの別のファイルに、同じfile_localという名前の関数があっても、それとは別の関数としてリンクされます。

これらの例の様に、同一ファイル内にまで関数のスコープを狭くする事ができますが、通常は、それ以上スコープを狭める事はできません。

無名でない名前空間を利用して、さらにスコープを狭める方法もありますが、関数内で利用するローカル関数の宣言に使うには、記述が美しくないので、ここでは名前空間は扱わない事にします。

2.ローカルクラスを使ったローカル関数の実現法

C++では、関数内で関数を定義する事はできないものの、関数内でクラスを定義するローカルクラスは許されています。関数内でクラスを定義すると、そのクラスは定義した関数内からしか見えません。

また、ローカルクラスもクラスの一種ですから、メンバ関数を持つ事ができます。ローカルクラスのメンバ関数は、クラスを定義した関数内にスコープが限定されますから、ローカルクラスのメンバ関数は、実質上のローカル関数(関数内関数)になります。

1つのメンバ関数を定義しただけの構造体(C++では構造体は、メンバがデフォルトでpublicになる、一種のクラス)を使ってローカル関数を実現した例が、第四報:怪しい関数内関数に紹介されていました。

リンク先の記事では、マクロを使って記述を見やすくしようとしていますが、マクロを使わずに同様の方法でローカル関数を定義すると、リスト4の様になります。

リスト4、main関数内でローカル関数LocalFunc::addを定義した例
#include <iostream>
using namespace std;

int main()
{
  // LocalFunc構造体は、add関数の定義に利用する
  struct LocalFunc {
    // add関数は2つの引数の和を返す
    // LocalFunc構造体のインスタンスは作らないので、静的メンバ関数にする
    static int add(int a, int b)
    {
      return a+b;
    }
  };

  cout<<LocalFunc::add(3,5)<<endl; // 3と5の和(8)を表示する
}

LocalFuncという構造体が、ローカル関数を定義するための構造体です。この構造体の内部でaddという、2つの引数の和を返すメンバ関数を定義しています。

add静的メンバ関数ですから、

LocalFunc::add(3,5)

の様に、前にLocalFunc::を付ければ呼び出せます。

LocalFunc構造体は、main関数内のローカル構造体ですから、main関数内からしか見えません。リスト4にはmain以外の関数がありませんが、もし他の関数があったとしても、そこからはLocalFunc構造体が見えず、よってLocalFunc::add関数も呼べません。

この様にローカル構造体のメンバ関数を用いる事で、ローカル関数が疑似的に実現できますが、この手法はArduino IDE 1.6.6に文字コード関係のバグ?という記事内のリスト9のConvStr関数でも利用しています。

リスト5の様に構造体内で複数のメンバ関数を定義すれば、1つの関数内で複数のローカル関数が使えます。

リスト5、複数のローカル関数を定義した例
#include <iostream>
using namespace std;

int main()
{
  // LocalFunc構造体は、add関数とmul関数の定義に利用する
  struct LocalFunc {
    // add関数は2つの引数の和を返す
     static int add(int a, int b)
    {
      return a+b;
    }

    // mul関数は2つの引数の積を返す
    static int mul(int a, int b)
    {
      return a*b;
    }
  };

  cout<<LocalFunc::add(3,5)<<endl; // 3と5の和(8)を表示する
  cout<<LocalFunc::mul(3,5)<<endl; // 3と5の積(15)を表示する
}

3.ローカルな関数オブジェクトを使って記述をきれいにする

先ほど紹介したLocalFuncという構造体を利用してローカル関数を実現した例では、関数のスコープを制限するという意味では、うまくローカル関数を実現できていたのですが、関数を呼び出す際に、いちいちLocalFunc::を付けないといけないのが面倒くさいです。また、特に意味のない構造体に、LocalFuncという名前をつけなければならないこと自体が美しくありません。

先日関数オブジェクトの勉強をしていたところ、関数オブジェクトを使う事で、ローカル関数の呼び出しの記述が綺麗になる事に気が付きました。

関数オブジェクトを使って実際にローカル関数addを定義した例をリスト6に示します。

リスト6、ローカルな関数オブジェクトを使ってローカル関数addを定義した例
#include <iostream>
using namespace std;

int main()
{
  // ローカル関数を定義するための無名のローカル構造体
  struct {
    // ()演算子をオーバーロードして関数オブジェクトにする
    int operator()(int a, int b)
    {
      return a+b;
    }
  } add; // addはローカル構造体の変数

  cout<<add(3,5)<<endl;
}

ローカル構造体の()演算子をオーバーロードして、ローカルな関数オブジェクトを作っています。

使い捨ての構造体ですから、構造体自体には名前を付けていません。また、構造体の定義と同時に、addという変数を宣言しています。

ローカル構造体は()演算子をオーバーロードしていますから、

add(3,5)

という具合に変数名の後に(3,5)を付けると、メンバ関数が、3と5の引数で呼ばれます。

この様に、ローカルな関数オブジェクトを使うと、普通の関数と同じ形式でローカル関数が呼べますし、ローカル関数の定義のための構造体に名前を付ける必要もありません。

ただ、関数オブジェクトを利用しますから、関数オブジェクトに慣れていない人は、関数の定義の部分が、ちょっと奇異に感じるかもしれません。

また、ローカル関数の定義をする際に、関数名が一番最後に来るのも、ソースの他の部分の書き方と違ってきます。

4.ローカルな関数オブジェクトを使って、再帰呼び出しのあるローカル関数を実現する

再帰呼び出しのあるローカル関数は、thisポインタを使えば記述できます。また、メンバ関数の名前はoperetor()ですから、それを使って記述する方法もあります。

ローカルな関数オブジェクトを使って、再帰呼び出しのあるローカル関数を記述した例をリスト7に示します。

リスト7、再帰呼び出しのあるローカル関数を定義した例
#include <iostream>
using namespace std;

int main()
{
  // ローカル関数を定義するための無名のローカル構造体
  struct {
    // 引数の階乗を計算する関数(再帰呼び出し使用)
    int operator()(int n)
    {
      if(n==0) {
        return 1;
      } else {
        return n*(*this)(n-1);  // return n*operator()(n-1); という書き方も可能
      }
    }
  } frac;

  cout<<frac(4)<<endl; // 4の階乗の24を表示
}

5.ローカル関数内のローカル関数

C++では、クラス内で、さらに別のクラスを宣言できます。

リスト8は、クラス内でクラスを宣言(厳密には構造体内で構造体を宣言)した例です。

リスト8、クラス内クラスの例
#include <iostream>
using namespace std;

struct AddSqr { // 外側の構造体
  struct Sqr { // 内側の構造体
    // 引数の2乗を返す関数
    static int func(int n) { return n*n; };
  };

  // a^2+b^2を返す関数
  static int func(int a, int b) { return Sqr::func(a)+Sqr::func(b); };
};

int main()
{
  cout<<AddSqr::func(3,4)<<endl; // 3^2+4^2の計算結果(25)を表示
  cout<<Sqr::func(3)<<endl; // エラー
  cout<<AddSqr::Sqr::func(3)<<endl; // こうすれば内側の構造体のメンバ関数を呼ぶ事もできる
}

AddSqr構造体の中で、別の構造体Sqrを定義しているのが分かると思います。main関数からはAddSqr::func関数を呼べますが、Sqr::func関数は呼べません。

注:ただし、AddSqr::Sqr::funcと記述すれば、内側の構造体のメンバ関数を呼び出すこともできますので、外部から完全に隠蔽できているわけではありません。

このクラス内クラス(構造体内構造体)の考え方と、関数内のローカルクラスの考え方と、関数オブジェクトの考え方をミックスすれば、リスト9の様に、ローカル関数の中に、さらにローカルな関数を定義する事ができます。

リスト9の場合は、変数Sqrのスコープが、外側の構造体の内部に制限されているため、Sqr関数は、AddSqr関数の外部から完全に隠蔽されています。

リスト9、ローカル関数内のローカル関数の例
#include <iostream>
using namespace std;

int main()
{
  // ローカル関数AddSqrの定義
  struct {
    // ローカル関数AddSqr内のローカル関数、Sqrの定義
    struct {
      int operator()(int n)
      {
        return n*n;
      };
    } Sqr;
    
    int operator()(int a, int b)
    {
      return Sqr(a)+Sqr(b);
    };
  } AddSqr;
  
  cout<<AddSqr(3,4)<<endl; // 3^2+4^2の計算結果(25)を表示
}

6.ローカル関数外の変数へのアクセス

ローカル関数で、ローカル関数の外の変数へアクセスできるのでしょうか?グローバル変数にアクセスする場合と、関数内のローカル変数へアクセスする場合とに分けて考えます。

6-1.グローバル変数へのアクセスは可能

グローバル変数はクラス内からでも参照できるため、リスト10に示す様に、ローカル関数は、グローバル変数には問題なくアクセスできます。

リスト10、グローバル変数へのアクセス
#include <iostream>
using namespace std;

int global=123; // グローバル変数

int main() {
  // ローカル関数ReadGlobalの定義
  struct {
    int operator()()
    {
      return global; // ローカル関数内で、グローバル変数globalにアクセス
    };
  } ReadGlobal;

  cout<<ReadGlobal()<<endl; // 123を表示
}

6-2.関数内のローカル変数へはstatic変数のみアクセス可能

次に、ローカル関数の外側の関数のローカル変数へのアクセスについて考えます。

リスト11のmain関数には、LocalAutoLocalStaticの2つのローカル変数があります。LocalAuto変数の方はstatic修飾子が付いていないので、auto変数(自動変数)になります。一方で、LocalStatic変数の方は、static変数になります。

LocalAuto変数も、LocalStatic変数も、ローカル関数のスコープには入っているのですが、C++ではローカルクラスからauto変数にはアクセスできないので、ローカル関数からLocalAuto変数にアクセスすると、コンパイル時にエラーになります。一方で、staticなローカル変数(LocalStatic変数)にはアクセスできます。

リスト11、関数内のローカル変数へのアクセス例
#include <iostream>
using namespace std;

int main() {
  int        LocalAuto  =123; // ローカル変数(auto)
  static int LocalStatic=456; // ローカル変数(static)

  // ローカル関数ReadLocalAutoの定義
  struct {
    int operator()()
    {
      return LocalAuto; // main関数のローカル変数にアクセス(エラー)
    };
  } ReadLocalAuto;

  // ローカル関数ReadLocalStaticの定義
  struct {
    int operator()()
    {
      return LocalStatic; // main関数のローカル変数にアクセス(成功)
    };
  } ReadLocalStatic;

  cout<<ReadLocalAuto()  <<endl; // エラー
  cout<<ReadLocalStatic()<<endl; // 456を表示
}

どうしても、関数とそのローカル関数とでauto変数を共有したい場合は、ローカル関数を定義するための構造体のメンバ変数を使えば、リスト12の様に(事実上の)auto変数を共有できます。ただ、記述が汚いのであまりお勧めできる方法ではありません。

リスト12、構造体のメンバ変数を使ってauto変数をローカル関数と共有する例
#include <iostream>
using namespace std;

int main() {
  // ローカル関数ReadLocalAutoの定義
  struct {
    // 関数とそのローカル関数で共有したいauto変数は、ローカル関数を定義するための構造体のメンバにする。
    // 関数オブジェクトのインスタンス(ReadLocalAuto)がauto変数なので、その(非static)メンバ変数はauto変数になる。
    int LocalAuto;

    // ローカル関数の定義本体
    int operator()()
    {
      return LocalAuto;
    };
  } ReadLocalAuto;

  // 必要ならば、確保した変数への参照を宣言しておくと、普通のauto変数っぽくアクセスできる。
  int &LocalAuto=ReadLocalAuto.LocalAuto;

  // これ以降は、LocalAutoを普通のauto変数として、ReadLocalAutoをLocalAutoを読むためのローカル関数として扱える。
  LocalAuto=123; // 実際にはReadLocalAuto.LocalAutoへの代入。
  cout<<ReadLocalAuto()<<endl; // 123が表示される。
}

7.その他の制限

このページで提案した手法では、ローカル関数内にさらにローカル関数がある場合に、内側のローカル関数から外側のローカル関数を呼ぶ事(一種の再帰呼び出し)はできません。

一方、例えばPascalでは、内側のローカル関数から外側のローカル関数を呼ぶ事が可能です。(リスト13参照)

リスト13、Pascalで内側の関数から外側の関数を呼ぶ例
program sample(input,output);

  { グローバル手続きtestの定義。pascalではコメントは中括弧でくくる }
  { 手続き(procedure)は、C++でいうところのvoid型の関数 }
  procedure test;

    { testの中で、ローカル手続きp1を定義 }
    procedure p1(a,b : integer);

      { p1の中で、ローカル手続きp2を定義 }
      procedure p2(a,b : integer);
      begin
        writeln('p2:',a,',',b);
        p1(b,a); { p1の呼び出し。外側のローカル手続きを呼んでいる }
      end;

    { 手続きp1の本体 }
    begin
      writeln('p1:',a,',',b);
      if a>0 then p2(a-1,b); { p2の呼び出し }
    end;

  { 手続きtestの本体 }
  begin
    p1(3,5); { testの呼び出し }
  end;

{ プログラム本体 }
begin
  test;
end.

また、このページの提案手法では、ローカル関数の中にさらにローカル関数がある場合、内側のローカル関数が外側のローカル関数のローカル変数を使う事はできません。この場合もPascalなら可能です。(リスト14参照)

リスト14、Pascalで内側のローカル関数が外側のローカル関数のローカル変数にアクセスする例
program sample(input,output);

  { グローバル手続きtestを定義 }
  procedure test;

    { testの中で、ローカル手続きp1を定義 }
    procedure p1;
    var i : integer; { p1のローカル変数。p2からもアクセス可能 }

      { p1の中で、ローカル手続きp2を定義 }
      procedure p2;
      begin
        i:=i+1; { 外側の関数のローカル変数にアクセス }
      end;

    { 手続きp1の本体 }
    begin
      i:=3;
      p2; { p2の呼び出し。iが1増える}
      writeln(i); { 4を表示 }
    end;

  { 手続きtestの本体 }
  begin
    p1; { p1の呼び出し }
  end;

{ プログラム本体 }
begin
  test; { testの呼び出し }
end.

8.Arduino IDEでの動作確認

この記事では、C++の関数内にローカル関数を実現する方法について説明してきましたが、この方法がArduino IDEでも使える事を確認しておきます。

リスト7の、階乗を求めるプログラムを、Arduino用に書き換えたのがリスト15です。ローカル関数を使う事で、setup関数内ですべての処理が完結しています。

リスト15、ローカルな関数オブジェクトを使って作ったローカル関数をArduino用のスケッチで使った例
void setup() 
{
  // 階乗を計算するローカル関数fracの定義
  struct {
    int operator()(int n)
    {
      if(n==0) {
        return 1;
      } else {
        return n*(*this)(n-1);
      }
    };
  } frac;
  
  Serial.begin(9600);
  while(!Serial);
  Serial.println(frac(4)); // 4の階乗(24)を表示
}

void loop() 
{
}

これを実行すると、図1の様に、シリアルモニタに24と表示されます。4!(=24)の計算のスケッチですから、期待通りの値が計算できています。なお、この動作試験には、Arduino IDE 1.7.10とArduino Leonardoを使いました。

図1、リスト14の実行結果
図1、リスト14の実行結果

9.最後に

私は元々Pascalでプログラムを良く組んでいたのですが、Pascalでは、ローカル関数は当たり前の様に作れます。言語がローカル関数に対応していないという意味では、C++はちょっとプログラムが書きにくく感じる事もありました。

ただ、このページで紹介した手法でローカルな関数オブジェクトを使う事により、疑似的なローカル関数を、比較的きれいに記述できます。ただし、説明したとおり、ローカル関数から外部の変数のアクセスには、制限もあります。

制限があるとはいえ、ローカル関数を使えると便利な局面はたくさんあります。関数内でちょっとした小さな関数を使いたい時には重宝するかもしれません。

ツイート
シェア
このエントリーをはてなブックマークに追加
フォロー

このページで使われている用語の解説

関連ページ

このサイトの記事が本になりました。
書名:Arduino 電子工作
ISBN:978-4-7775-1941-5
工学社の書籍の内容の紹介ページ
本のカバーの写真か書名をクリックすると、Amazonの書籍購入ページに移動します。
サイトマッププライバシーポリシーお問い合わせ用語集
しなぷすのハード製作記