はじめに
本記事では、C++において、関数から複数の戻り値を返す方法を3つご紹介します。各方法の良し悪しをまとめ、個人的におすすめな方法をご紹介いたします。また、各方法の詳細やサンプルコードを提示いたします。
複数の戻り値を返す3つの方法
- 出力引数
- 呼び出された関数は、自身の引数に出力値を代入する
- 関数の呼び出し側は、戻り値用の引数を、関数の引数として渡す
- 構造化束縛 (C++ 17 から使用可能)
- 呼び出された関数は、組やタプル、配列や構造体などを生成し、値を詰めて返却する
- 関数の呼び出し側は、戻り値を分解して各要素を取り出す
- 値オブジェクト
- 関数の戻り値を格納する専用のclassを定義する
- 呼び出された関数は用意したclassに値を詰め込む
- 関数の呼び出し側は、値オブジェクトのgetterを呼び出して、値を取得する
評価項目
また、下記の観点に基づき、3つの方法を評価します。
- 記述量の少なさ
- 複数の戻り値を返すために記述するコードの行数が少ないほど高評価とします
- 変更時の安全性
- 戻り値を追加、削除したとき、戻り値の定義順を並べ替えるなどの操作を行ったときに、意図せぬ不具合が発生しにくいほど高評価とします
- 引数の不変性(immutable)
- 関数の引数が全てconstで定義できる場合 ○、できない場合は × とします
- 引数の不変性が保証されている場合、意図しない再代入を防ぐことができ、関数の副作用を減らすことができます
各項目の評価は、筆者個人の主観で、◎○△×の4段階表記としました。評価の目安は下記のとおりです。
- ◎ ... 優れている
- ○ ... 良い
- △ ... 懸念点はあるが許容範囲
- × ... 劣っている
評価結果
出力引数 | 構造化束縛 | 値オブジェクト | |
---|---|---|---|
記述量の少なさ | △ | ○ | × |
コード変更時の安全性 | × | △ | ◎ |
引数の不変性 | × | ○ | ○ |
個人的おすすめ度 | ☆ | ☆☆ | ☆☆☆ |
評価結果は、上表のようになりました。
業務で開発する上で一番おすすめしたいのは、値オブジェクトを返却する方法です。これは、変更時の安全性が一番高いと考たためです。次点は構造化束縛です。こちらは記述量を少なくできるため、個人開発や小規模な開発であればシンプルに書けるのではと考えたためです。
サンプルコードのお題
本記事では、整数の割り算を例として、戻り値を返す3つの方法を説明していきます。
$$ a / b = q \ldots r $$
- ※ ただし、a: 割られる数, b: 割る数, q: 商, r: 剰余とする。
- ※ 例:
5 ÷ 3 = 1 あまり 2
3つのパターン
1. 出力引数
概要
はじめに、オーソドックスな方法である出力引数を使う方法について説明します。
まず、呼び出される関数 calcQuotientAndReminder()
の定義を見てみましょう。
出力引数は、関数の引数に出力用の引数を定義します。下記のサンプルコードでは、int& q, int& r
のように出力変数を参照渡しで定義します。
入力引数はconst
をつけ、関数内で再代入されないようにします。
サンプルコード
/**
* @fn
* a ÷ b の商と余りを計算する
* @param [in] a 割られる数
* @param [in] b 割る数
* @param [out] q 商(quotient)
* @param [out] r 余り(reminder)
*/
void calcQuotientAndReminder(const int& a, const int& b, int& q, int& r)
{
// 関数内で引数に出力値を代入する
q = a / b;
r = a % b;
}
/**
* @fn
* 呼び出し側
*/
int main()
{
const int a = 5;
const int b = 3;
// 呼び出し側で出力用の変数を用意する
int q;
int r;
calcQuotientAndReminder(a, b, q, r);
std::cout << a << " ÷ " << b << " = " << q << " 余り " << r << std::endl;
// 出力
// >> 5 ÷ 3 = 1 余り 2
return 0;
}
メリット
生成した変数に値を代入するため、メモリ効率が良い場合があります。
デメリット
呼び出し側で出力用の変数を用意する必要があり、かつ関数内で引数に再代入する方法なので、変更箇所が分かりにくいというデメリットがあります。また、呼び出し側で出力引数にconst
宣言をつけることができないため、意図せぬ再代入を防ぐことができません。
2. 構造化束縛
概要
次に、C++ 17で利用可能になった構造化束縛について説明します。
呼び出される関数 calcQuotientAndReminder()
の引数が4個から2個に減りました。出力引数で定義していたint& q, int& r
が不要になったためです。
構造化束縛を使用する場合、呼び出される関数内で、複数の値を格納できるコンテナや構造体を生成します。例として、タプルなどがあります。呼び出し側では、戻り値に含まれる要素数と同じ個数の変数を用意し、戻り値の要素を変数に代入します。
サンプルコード
/**
* @fn
* a ÷ b の商と余りを計算する
* @param [in] a 割られる数
* @param [in] b 割る数
* @return 割り算の商と余り
*/
std::tuple<int, int> calcQuotientAndReminder(const int& a, const int& b)
{
const int q = a / b;
const int r = a % b;
// 戻り値用のtupleを生成し、返却する
return { q, r };
}
/**
* @fn
* 呼び出し側
*/
int main()
{
const int a = 5;
const int b = 3;
// 構造化束縛
const auto [q, r] = calcQuotientAndReminder(a, b);
std::cout << a << " ÷ " << b << " = " << q << " 余り " << r << std::endl;
// 出力
// >> 5 ÷ 3 = 1 余り 2
return 0;
}
メリット
出力引数が関数定義から消えるため、関数が読みやすくなります。TypeScript等で見られる分割代入と似た概念なので、他言語経験者がコードを読んだ時の違和感が小さくなります。また、呼び出し側では、戻り値を格納する変数にconst
宣言をつけることができ、immutableにすることができます。
デメリット
戻り値にSTLのコンテナを使用する場合、ソースコード変更時にやや注意が必要かと思います。例えば、戻り値の変数型としてstd::array
を選択したとします。呼び出される関数で、戻り値の要素の順番を変えた(q, r を r, q の順に変更した)場合、呼び出し側も値の取り出し順を変更しないといけません。もし、呼び出し側での変更を忘れてもコンパイルは通過します。このように、関数の戻り値を変えた場合、呼び出し側も注意が必要になります※1。
(※1 解決策の一つとして構造体を使う方法もありますが、本記事では省略します)
3. 値オブジェクト
概要
最後に、値オブジェクトを定義する方法をご紹介します。個人的には一番おすすめの方法です。
まず、OutputValueObject
という名前のclassを定義します。本記事では、このclassのインスタンスを値オブジェクトと呼びます。OutputValueObject
が公開しているのはコンストラクタとgetterのみです。よって、OutputValueObject
の値はインスタンス生成時のみ設定でき、その後は変更ができません (immutable)。
呼び出される関数 calcQuotientAndReminder()
は、自身の内部で値オブジェクトを生成します。呼び出し側では、戻り値の値オブジェクトのgetterを呼び出して値を取得します。
サンプルコード
/**
* @class
* 商と余りを格納する値オブジェクト
*/
// こちらはclassではなく、structでの定義でも問題ないです。(コメントを反映しました)
class OutputValueObject {
int quotient; // 商
int reminder; // 剰余
public:
OutputValueObject(const int& q, const int& r) : quotient(q), reminder(r) {};
~OutputValueObject() {};
int getQuotient() const { return quotient; }
int getReminder() const { return reminder; }
};
/**
* @fn
* a ÷ b の商と余りを計算する
* @param [in] a 割られる数
* @param [in] b 割る数
* @return 割り算の結果を格納した値オブジェクト
*/
OutputValueObject calcQuotientAndReminder(const int& a, const int& b)
{
const int q = a / b;
const int r = a % b;
// 戻り値用の値オブジェクトを生成し、返却する
return OutputValueObject(q, r);
}
/**
* @fn
* 呼び出し側
*/
int main()
{
const int a = 5;
const int b = 3;
const auto output = calcQuotientAndReminder(a, b);
// 値オブジェクトのgetterを使って値を取得する
auto q = output.getQuotient();
auto r = output.getReminder();
std::cout << a << " ÷ " << b << " = " << q << " 余り " << r << std::endl;
// 出力
// >> 5 ÷ 3 = 1 余り 2
return 0;
}
メリット
この方法は、既存コードの変更を最も安全に行える方法だと考えています。例えば、呼び出される関数の戻り値を追加したい場合は、値オブジェクトのメンバを増やせば良いです※2。呼び出し側は、メンバ変数の定義順などを気にする必要はありません。
デメリット
本記事で紹介した3つの方法のうち、最も記述量が多いのがこの方法です。
(※2 値オブジェクトの生成時に、コンストラクタの引数の代入順を間違える可能性がありますが、これは戻り値を返す関数側の責務とします。)
まとめ
本記事では、入力引数, 構造化束縛, 値オブジェクト の3つの方法を紹介し、それぞれのメリデメを比較しました。私個人の感想になりますが、ソースの規模が大きく、複数人で開発する場合は、値オブジェクトを定義する方法が最も良いと考えます。ただし記述量が多いので、簡単なアプリであれば、構造化束縛を使用する方法も良いのではないでしょうか。
評価結果(筆者の主観含む)
出力引数 | 構造化束縛 | 値オブジェクト | |
---|---|---|---|
記述量の少なさ | △ | ○ | × |
変更時の安全性 | × | △ | ◎ |
引数の不変性 | × | ○ | ○ |
個人的おすすめ度 | ☆ | ☆☆ | ☆☆☆ |