C++ での前方宣言

Dhruvdeep Singh Saini 2023年10月12日
  1. C++ での前方宣言
  2. C++ での関数の前方宣言
  3. C++ でのクラスの前方宣言
  4. C++ コンパイラが前方宣言を必要とする理由
  5. C++ で前方宣言を使用する利点
C++ での前方宣言

この記事では、前方宣言について説明し、C++ のコンパイラーに前方宣言が必要な理由とコード例を示します。

また、前方宣言を使用する利点について説明し、宣言と定義の違いを強調し、前方宣言を使用して C++ ファイルの循環依存エラーを回避する方法を示します。

C++ での前方宣言

前方宣言は、関数の構文の宣言です。つまり、プログラムで使用する前に、関数の名前、戻り値の型、引数、および引数のデータ型を宣言します。

関数を定義する前に、関数がプログラムのどこかで定義されていることをコンパイラーに知らせるための前方宣言を含めます。別のファイルで使用される関数の前方宣言は、ファイルを持つために #include を使用して形成されます。

C++ での関数の前方宣言

コードスニペット内で前方宣言がどのように機能するかを見てみましょう。

#include <iostream>
using namespace std;

// forward declaration of sub2
int sub2(int A, int B);

int main() {
  cout << "Difference: " << sub2(25, 10);
  return 0;
}

int sub2(int A, int B)  // Defining sub2 here
{
  return A - B;
}

出力:

Difference: 15

ここに、渡された 2つの int パラメーターの差を返す sub2 という名前の関数があります。主要部分の前に sub2 を宣言し、プログラムの後半で関数を定義します。

説明に入る前に、C++ での定義と宣言の違いを理解することが不可欠です。

  1. 宣言:宣言は、関数の名前、引数とそのデータ型、戻り型、つまり関数プロトタイプなどの簡単な情報を宣言することを提供します。
  2. 定義:定義は、関数宣言の詳細を提供し、タスク関数が実行するコードスニペットを含みます。

それでは、前方宣言に戻りましょう。上記のプログラムで sub2 の前方宣言が必要だったのはなぜですか?

今回は、前方宣言を使用せずに、同じコードで説明しましょう。

#include <iostream>
using namespace std;

int main() {
  cout << "Difference: " << sub2(25, 10);
  return 0;
}

int sub2(int A, int B)  // Defining sub2 here
{
  return A - B;
}

出力:

 error: 'sub2' was not declared in this scope
    6 |     cout << "Difference: " << sub2(25, 10);
      |                               ^~~~

上記のプログラムは問題ありませんが、関数 sub2 が宣言されていないというエラーが表示されます。これは、sub2 が 6 行目で呼び出されているが、10 行目以降まで定義されていないためです。

C++ はトップダウンの解析言語であるため、上から解析ツリーを構築し、使用する前に関数について事前に知っておく必要があります。呼び出される前に関数を定義する必要はありませんが、宣言する必要があります。

このようなエラーを回避するために、main 関数の前に関数(ここでは sub2)を定義することもできます。ただし、相互に呼び出す複数の関数または外部に含まれるファイルを含むプログラムでは、エラーが持続するため、前方宣言を使用します。

C++ でのクラスの前方宣言

また、C++ でのクラスの前方宣言も必要です。その方法を示しましょう。

#include <iostream>
using namespace std;

// Forward declaration of classes One and Two
class One;
class Two;

class One {
  int y;

 public:
  void num(int a)  // Getting input number
  {
    y = a;
  }
  friend int sub2(One, Two);
};
class Two {
  int x;

 public:
  void num(int a)  // Getting input number
  {
    x = a;
  }
  friend int sub2(One, Two);
};
int sub2(One a, Two b)  // Subtraction of two numbers from both classes
{
  int ans = a.y - b.x;
  return ans;
}

int main() {
  Two y;
  One x;

  x.num(25);
  y.num(10);

  cout << "Difference: " << sub2(x, y);
  return 0;
}

出力:

Difference: 15

上記のコードスニペットには、OneTwo のクラスが含まれています。どちらも、値を取得するための num 関数と、2つの数値を減算するための sub2 関数を備えています。

上記のプログラムでは、両方のクラスの前方宣言が必要です。これは、クラス One に、パラメーターにクラス Two が指定されたフレンド関数 sub2 が含まれているためです。

上記のコードスニペットでクラスの前方宣言を削除すると、エラーメッセージが表示されます。

15 |    [Error] 'Two' has not been declared In function 'int sub2(One, Two)':

このエラーは、コンパイラがプログラムで使用する前に、関数とクラスの前方宣言が必要であることを示しています。

C++ コンパイラが前方宣言を必要とする理由

前方宣言は、コンパイラーが次の 3つのことを保証するのに役立つため必要です。

  • プログラムは正しく、トークンのスペルミスはありません。
  • 宣言された関数の引数は正しいです。
  • 宣言された関数はプログラムに存在し、以下で定義されています。

関数を前方宣言しなかった場合、コンパイラーは、関数が何であるかに関するさまざまな推測を含む情報を含む追加のオブジェクトファイルを作成します。

また、リンカ(複数のオブジェクトとクラスを単一の実行可能オブジェクトファイルにリンクするプログラム)には、同じ名前の既存の関数が異なるデータ型の引数を持つ可能性があるため、リンケージの問題が発生します。

たとえば、関数 int sub2(int a, int b) があるとします。前方宣言がないと、リンカは別の既存の関数 int sub2(float a, float b) と混同される可能性があります。

コンパイラーは、C++ 前方宣言を使用してクリーンなファイルのコードを検証します。場合によっては、C++ がそのようなプログラムをコンパイルして実行する可能性があることを覚えておくのが最善です。

ただし、期待される出力は提供されません。これが、コンパイラがコードで前方宣言を実装または使用する前に前方宣言を必要とする理由です。

C++ で前方宣言を使用する利点

前方宣言は、コンパイラーがコードをより適切に検証し、リンケージの問題を回避するのに役立ちます。しかし、それはまた役立ちます:

  1. 名前空間の汚染を回避する:前方宣言は、コードスニペットの置き忘れを防ぎ、名前空間の汚染を回避するのに役立ちます。
  2. コンパイル時間の改善:ヘッダーファイルをインクルードすることにより、C++ プログラムに関数の宣言を追加できます。コンパイラーは、ファイルで提供されるすべてのトークンを解析しますが、これには長い時間がかかる場合があります。ただし、この時間のかかる処理を回避し、cpp ファイル全体の代わりに、使用する予定の特定のクラスに対して前方宣言を使用することができます。

これは小さなコードには影響しないかもしれませんが、コンパイル時間を最小限に抑えて時間の複雑さを軽減できるため、より重要なプロジェクトで役立ちます。したがって、C++ ファイル全体を含める代わりに、拡張子が .h の特定のクラスを使用できます。

  1. 名前の衝突の回避:前方宣言は、関数名またはクラス名が一致する異なるプロジェクトがある場合に、プログラム内でトークン名またはプリプロセッサー名の衝突がないことを確認するのにも役立ちます。
  2. 循環依存の解消:クラスの前方宣言は、ファイルに必要な特定の部分を宣言し、ヘッダーを除外することで循環参照を解決できます。

C++ で前方宣言を使用して循環依存を回避する

相互に関連する、または相互の関数を使用する 2つのクラスは、循環関係を作成します。これは、循環依存または循環依存として知られています。

プログラム内に 2つのクラスがあり、両方がもう一方を使用する必要があるとします。その場合、一方のヘッダーファイルを追加しますが、さらにもう一方の循環依存クラスのヘッダーファイルをインクルードしようとし、各ヘッダーがもう一方のヘッダーを取得しようとするサイクルを作成します。

循環依存を回避する方法を見てみましょう。

#include <iostream>
#include <vector>

#include "Two.h"  // Defining Two as it is used in One

class One {
  std::vector<Two> two;
};

int main() { return 0; }

私たちのプログラムには、クラス One で使用されている #include を使用した Two.h という名前の別のファイルが含まれています。上で説明したように、他のプログラム全体ではなく class.h ファイルを含めると、コンパイル時間が大幅に短縮されます。

さて、Two.h の内容を見てください。

#include "One.h"  // Defining One as it is used in Two

class Two {
  One* a;  // Two uses a pointer of One
};

Two.h には、クラス One のポインタを使用するクラス Two が含まれています。ただし、両方のファイルにもう一方のファイルを含めるためのヘッダーが含まれているため、両方のファイルが相互に呼び出し続ける循環依存関係に陥ります。これは、次の方法で Two.h にヘッダーを追加する代わりに、前方宣言を使用することで回避できます。

#include <iostream>

class One;

class Two {
  One* a;  // Two uses a pointer of One
};

関数の前方宣言には、関数の定義時に使用される関数の名前と引数の知識が必要であり、デフォルトのパラメーターのデフォルト値を複製する必要があることを覚えておくのが最善です。

したがって、プログラムで前方宣言を使用する場合は注意が必要です。