関数から値を返す | Programming Place Plus 新C++編

トップページ新C++編

このページの概要

このページでは、前のページに続いて「関数」について取り上げます。今回は、前のページでは深入りしなかった戻り値を中心に説明します。戻り値は、関数が呼び出し元に返す値のことで、処理の結果や、現在の状態、エラーの有無などの情報を呼び出し元に伝える方法として使えます。

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



戻り値

前のページで関数を作る方法を取り上げました。そこでは、関数の側から呼び出し元へ返す値である戻り値に関しては、軽い紹介にとどめました。このページでは、その戻り値について集中的に説明します。

関数を宣言する構文には以下の2つがありました。

戻り値の型 識別子(仮引数);
auto 識別子(仮引数) -> 戻り値の型;

「戻り値の型」のところに、呼び出し元へ返したい値の型を記述します。戻り値は1個だけしか返せないので、ここに記述する型も1つだけです。複数の値を返したいこともありえますが、そのような場合の対処方法はあとで取り上げます。戻り値が不要な場合には void と記述します。

戻り値を返すには、関数の中で return文を使います。たとえば return 0; とすれば 0 を返すことになりますし、return a + b; とすれば a + b の結果を返すことになります。いずれにしても「戻り値の型」と同じ型にならなければなりません(可能ならば、暗黙の型変換が行われます)。

戻り値の型を void とした場合の return文は return; としか書けません。また、戻り値の型が void でないのに、return; とだけ書くことはできません。

関数の中で宣言した変数の値を返すことができますが、参照型として返すのは重大なバグになるので注意が必要ですあとで取り上げます)。

仮引数と違って、戻り値には名前を付けられませんから、呼び出す側が、どんな意味の戻り値が返ってくるのか分かるようにすることが望まれます。1つには、関数名を分かりやすくする方法がありますが、限界もあるので、コメントを書き添えておくのが妥当な手段です。戻り値の意味のほか、引数の意味やルール、その関数がどんなことをするものなのかといったことを書きます。呼び出し側から確実にみえるところにあるのは関数の宣言のほうなので、これらのコメントは、関数の宣言のところに書きます(定義しかない場合は定義のところでもいい)。


次のプログラムの getline_from_stdin関数は、標準入力から1行分の文字列を取得して、戻り値で返しています。

#include <iostream>
#include <string>

// 標準入力から1行分の文字列を受け取る。
// 戻り値: 受け取った文字列
std::string getline_from_stdin()
{
    std::string s {};
    std::getline(std::cin, s);
    return s;
}

int main()
{
    std::cout << "Please enter the string.\n";
    std::string s {getline_from_stdin()};
    std::cout << s << "\n";
}

実行結果:

Please enter the string.
Hello, World!  <-- 入力した文字列
Hello, World!

関数の呼び出し元は、戻り値を必ずしも変数で受け取らないといけないわけではなく、返された値をほかの関数の実引数に使うといったことも可能です。

std::cout << getline_from_stdin() << "\n";  // 戻り値を出力する
f(getline_from_stdin());  // 戻り値をほかの関数の実引数に使う

【上級】そうはみえないかもしれませんが、1行目のほうも、戻り値を関数の実引数として使っています。<< の正体は operator<< という名前の関数で、getline_from_stdin() はその実引数なのです。

また、戻り値の型が持っている関数やデータメンバをそのまま使うコードも可能です。

// 返されてきた std::string の length関数を呼ぶ
std::cout << getline_from_stdin().length() << "\n";

// 返されてきた Book構造体のデータメンバをアクセスする
std::string author {get_book("title").author};

戻り値を無視する

関数が戻り値を返すように実装されていても、呼び出し側はその戻り値が不要だというケースがあります。たとえば、std::getline関数には戻り値があって、1個目の引数の値をそのまま返すという仕様になっています1 2。ほとんどの場合、これを受け取っても使い道がないため無視します。

しかし、戻り値を無視することが明らかにおかしいといえる関数もあります。たとえば、std::string の length関数は文字列の長さを戻り値で返しますが、この関数を呼び出しておきながら、戻り値はいらないというケースはあり得なさそうです。しかし戻り値を受け取らないという選択が許されている以上、次のようなコードが書けてしまい、バグの原因になります。

std::string s {"Hello"};
std::string::size_type length {};

s.length();  // 戻り値を受け取るのを忘れている
std::cout << length << "\n";  // それでも変数length 自体は存在しているからアクセスできる (だが 0 しか入っていない)

【C++17】戻り値を無視させてはならないことをコンパイラに教える [[nodiscard]]属性という機能が追加されました3。この属性を関数の宣言に付加しておくと、戻り値を無視する呼び出し方をした場合に警告が出るようになります。

【C++20】[[nodiscard]]属性に機能が加わり、戻り値を無視してはならない理由を記述できるようになりました。4

戻り値と参照型

戻り値の型を参照型にすることは可能ですが、これには危険な点があります。

このページの最初に挙げた getline_from_stdin関数の戻り値の型を参照型に変更してみます。

#include <iostream>
#include <string>

std::string& getline_from_stdin()
{
    std::string s {};
    std::getline(std::cin, s);
    return s;  // s はもう消えてしまうので、参照型で返してはならない
}

int main()
{
    std::cout << "Please enter the string.\n";

    std::string s {getline_from_stdin()};  // s を初期化するために getline_from_stdin関数が返した別名を使おうとするが、
                                           // その元になった変数はもう消えている。未定義の動作。
    
    std::cout << s << "\n";
}

このプログラムは、コンパイラによってはコンパイルエラーになりますが、警告されるだけの場合もあります。

戻り値の型が参照型なので、return s; は、変数s の別名を返そうとしていることになります。しかし、関数内で宣言された変数が使えるのは、その関数内に限られていますから、呼び出し元に処理が戻ったときには、別名の元になった変数はもう使えません。使えなくなった変数にアクセスする行為は未定義の動作です。

参照型を返す関数そのものが不正であるということではなく、問題なく実装できる場合もあります。たとえば、参照型の仮引数を参照型のまま返す場合、その元になった変数は呼び出し元のほうで存在し続けているので問題ありません。

関数内で宣言された std::vector や std::string などの要素を指すイテレータを返す行為も、同じ理由で問題があります。関数から抜け出してしまうと、指し示す先の要素はもう存在しないため、デリファレンスすると未定義の動作になります。

#include <iostream>
#include <string>

std::string::iterator get_first_character_from_stdin()
{
    std::string s {};
    std::getline(std::cin, s);
    return std::begin(s);  // イテレータが指し示す先の要素はもう消えてしまうので危険
}

int main()
{
    auto it = get_first_character_from_stdin();
    std::cout << *it << "\n";  // 未定義の動作
}

戻り値の型推論

戻り値の型を明示的に書かずに、コンパイラによる型推論に任せる方法があります。そのためには、戻り値の型を auto にします。

auto getline_from_stdin()
{
    std::string s {};
    std::getline(std::cin, s);
    return s;
}

戻り値を型推論させる場合、戻り値の型は、return文が何を返しているかによって決まります。上の getline_from_stdin関数の例なら、変数s が std::string型なので、std::string型であると推論されますし、何も返していない場合は void になります。また、return文が複数ある場合は、共通の型を判断して決定されますが、そのような型がない場合はコンパイルエラーになります。

return s; という文からは、std::string型を返したいのか、実は参照型(std::string&)で返したいのかは読み解けませんが、「イテレータ」のページでも説明したとおり、auto による型推論の結果は参照型にはなりません。そのため、必ず std::string型に推論されます。

auto getline_from_stdin()
{
    std::string s {};
    std::getline(std::cin, s);

    std::string& rs {s};
    return rs;  // rs は参照型だが、auto による型推論の結果は std::string
}

もし参照型に型推論されることを望むのなら、戻り値の型を auto の代わりに auto& と書きます。また、戻り値の型を後ろに書く構文(「関数を作る」のページを参照)を使う場合は、次のように記述できます。

auto getline_from_stdin() -> auto&
{
    // 前のコードと同じ
}

なお、繰り返しになりますが、関数内で宣言された変数を参照型で返すことには危険があることに注意してください。

【上級】あるいは decltype(auto) と記述する方法もあります。

戻り値の型推論を使う場合、その関数の宣言からは戻り値を判断できないため、呼び出し元から関数の定義がみえている必要があります。

auto getline_from_stdin();  // 宣言からは戻り値の型が推論できない

int main()
{
    std::string s = getline_from_stdin();  // コンパイルエラー。戻り値の型がわからない
}

auto getline_from_stdin()
{
    std::string s {};
    std::getline(std::cin, s);
    return s;
}

複数の値を返す

戻り値は1つしか返せないという制約がありますが、複数の値を返したいケースも当然あります。実のところ、複数の値を返す方法は色々あるので、特に困ることはないです。たとえば、構造体型には複数のデータメンバがあるので、構造体型の値を返すのも1つの手ですし、型が同じなら std::vector<T> で返すこともできます。

Rectangle get_canvas_area();  // Rectangle 構造体型で返す
std::vector<double> generate_random_values();  // std::vector<double> 型で返す

こういう場合、戻り値が大きくなりがちですが、前述のとおり、参照型で返さないように注意してください

前のページで触れたように、仮引数を参照型にすることで、関数の中から呼び出し元の変数に値を入れてやることもできます。

// values から最大値と最小値をみつけて、min と max に入れる
void get_min_max(const std::vector<int>& values, int& min, int& max);

この方法は、呼び出し側のコードが get_min_max(v, min, max) のような記述になります。この例では、関数名から簡単に想像が付きますが、一見して、変数min、max の値が書き換えられているようにはみえないため、やや分かりづらくなる問題があります。また、関数を呼び出すより前に、変数min、max の宣言をしておかなければならないことが不便ですし、宣言時に適当な値を入れた状態になってしまうことがやや安全性に欠けるともいえます。

std::pair

get_min_max関数のように2つの値を返したいとき、std::pair を使う方法があります。std::pair は std::vector などと同じく、標準ライブラリで定義されている型の1つで、2つの値のペアを保持できます。その2つの値の型は異なっても構いません。std::pair を使うには、#include <utility> が必要です。

std::pair の全容についてはリファレンスサイト(cpprefjpcppreference.com)を参照してください。

次のプログラムは、get_min_max関数を std::pair を使って実装したものです。

#include <iostream>
#include <limits>
#include <utility>
#include <vector>

// std::vector<int> から、最小値と最大値を取得する。
// values: 対象の配列。要素が空の場合は、正常な結果を得られない。
// 戻り値: values の要素の最小値と最大値のペア
std::pair<int, int> get_min_max(const std::vector<int>& values)
{
    std::pair<int, int> min_max {
        std::numeric_limits<int>::max(),
        std::numeric_limits<int>::min()
    };

    for (int e : values) {
        if (min_max.first > e) {
            min_max.first = e;
        }
        if (min_max.second < e) {
            min_max.second = e;
        }
    }

    return min_max;
}

int main()
{
    std::vector<int> values {2, 4, 7, 3, 4, 5};
    auto min_max = get_min_max(values);
    std::cout << min_max.first << ", " << min_max.second << "\n";
}

実行結果:

2, 7

戻り値の型を return文のところから型推論できるので、auto get_min_max(const std::vector<int>& values) ともできます。

std::pair は <> のあいだに2つの型名を記述します。今回は2つとも int型にしたいので、std::pair<int, int> とします。

std::pair はどんな意味をもった値にでも使えるようになっており、保持している2つの値は firstsecond という、あまり意味を主張しない名前を使ってアクセスするようになっています。std::pair<A, B> なら、型A の方が first、型B の方が second ということになります。std::pair の弱点はここにあって、もっとはっきりした名前を付けたほうがいい思うのなら、構造体型を定義したほうがいいでしょう。

なお、std::pair どうしで比較は、保持する2つの値の型まで同じなのであれば可能です。代入に関しても同様です。

【C++17】auto [min, max] = get_min_max(values); という記述で、2つの変数min、max を宣言しつつ、get_min_max関数が返した std::pair の値で初期化できるようになりました。これは構造化束縛5という機能です。

std::pair は2つの値のペアしか扱えませんが、3つ以上あつかえる std::tuple も存在します。ごく基本的なところでは使い方や考え方は同じですが、実際にはいくらか関連して解説する事項があって長くなってしまうので、ここでは取り上げないことにします。必要であれば、リファレンスサイト(cpprefjpcppreference.com)で確認してください。

初期化子リスト

戻り値を返すときに、初期化子リストを使うことができます。引数に使うときには、std::initializer_list を使いましたが(「関数を使う」のページを参照)、戻り値の場合は、戻り値の型が初期化子リストで初期化可能であるかどうかがポイントになります。

get_min_max関数の例で試してみます。

std::pair<int, int> get_min_max(const std::vector<int>& values)
{
    int min {std::numeric_limits<int>::max()};
    int max {std::numeric_limits<int>::min()};

    for (int e : values) {
        if (min > e) {
            min = e;
        }
        if (max < e) {
            max = e;
        }
    }

    return {min, max};  // 初期化子リストを記述
}

return {min, max}; が初期化子リストを使った構文です。戻り値の型である std::pair<int, int> の変数が暗黙的に作られ、その初期化子として {min, max} を与えていることになります。つまり、std::pair<int, int> xxx {min, max}; というリスト初期化が可能であるから許される構文です。std::vector や構造体型(集成体とみなせる場合)などの場合にも利用できます。

初期化子リストによる初期化(リスト初期化)のルールは、「構造体」のページで取り上げました。

なお、初期化子リストを返す return文からは、戻り値を型推論できません。

【上級】auto x = {0, 1, 2}; は、std::initializer_list<int> に型推論できるので、ルールが一貫していないともいえますが、配列を返す関数が作れない(ポインタに変換される)ことに合わせたためです6

複数の値をばらばらに受け取る

get_min_max関数の戻り値を受け取るとき、auto min_max = get_min_max(values); としました。この場合、値を受け取ったあとも std::pair のまま取り扱うことになります。前述したとおり、2つの値には first とか second といった具体性のない名前でアクセスしなければなりません。min_max は比較的意味は通じやすいものの、実際には「最安値と最高値」のような意味があるかもしれませんし、「計算結果とエラーフラグ」のような、直接的には関係性のない値のペアである可能性もあります。

受け取った std::pair の内容を、具体性のある名前の変数に移しかえれば分かりやすくはなりますが、無駄な処理が増えてしまいます。

auto min_max = get_min_max(price_data);  // いったん受け取る
int cheapest_price {min_max.first};      // コピーを作る。処理コストがややもったいない
int highest_price {min_max.second};      // 同上

もし、std::pair で返されたときに、受け取り側の意思で別個の変数に受け取れれば効率的です。完璧な解法とまではいえませんが、その実現に std::tie関数を使用できます。std::tie関数を使うには、#include <tuple> が必要です。

#include <iostream>
#include <limits>
#include <tuple>
#include <utility>
#include <vector>

// std::vector<int> から、最小値と最大値を取得する。
// values: 対象の配列。要素が空の場合は、正常な結果を得られない。
// 戻り値: values の要素の最小値と最大値のペア
std::pair<int, int> get_min_max(const std::vector<int>& values)
{
    int min {std::numeric_limits<int>::max()};
    int max {std::numeric_limits<int>::min()};

    for (int e : values) {
        if (min > e) {
            min = e;
        }
        if (max < e) {
            max = e;
        }
    }

    return {min, max};
}

int main()
{
    std::vector<int> price_data {1800, 30000, 240, 27000, 19500};

    int cheapest_price {};
    int highest_price {};
    std::tie(cheapest_price, highest_price) = get_min_max(price_data);  // std::pair の内容を2つの変数に分解

    std::cout << cheapest_price << ", " << highest_price << "\n";
}

実行結果:

240, 30000

std::tie関数の関数呼出しの記述のうしろに = があって代入式が続いています。右辺側にある値の組の内容を1つ1つ分解して、std::tie関数の実引数に書き並べた変数へ代入するという意味になります。

この方法は、std::pair などいくつかの限られた型でしか使えません。たとえば、構造体型のデータメンバを分解して受け取るといったことはできません。

【C++17】C++17 で追加された構造化束縛5はより良い方法で、auto [cheapest_price, highest_price] = get_min_max(price_data); のように1文にまとめられます。この1文で2つの変数を宣言し、右辺側にある値の組の内容が分解されて、それぞれの変数の初期化に用いられます。

また、複数の値のうち一部が不要であるというケースでは、std::tie関数の実引数を std::ignore にします。

int value {};
std::tie(value, std::ignore) = f();  // 返された std::pair<int, int> のうち、2つ目のほうは受け取らない

main関数の戻り値

main関数の戻り値の型は原則として int型です。ほかの型が使える可能性はありますが、それは処理系定義です7

main関数にかぎっては、戻り値の型が void でなくても return文を省略でき、関数の末尾に return 0; があるかのように扱われます。

【上級】main関数の内側の return文は、戻り値を実引数として std::exit関数を呼び出すことと同じ意味になります。return文を省略した場合は、return 0; と同じなので、std::exit(0); をしていることと同じです。8

まとめ


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク


練習問題

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

次の関数 f1、f2、f3、f4 の戻り値の型は何になりますか?

auto f1()
{
    std::pair<int, std::string> result {};
    // ...
    return result;
}

auto f2(int value)
{
    if (value < 0) {
        return -value;
    }
    return value;
}

auto f3(std::vector<int>& vec)
{
    for (int& e : vec) {
        e = 0;
    }
}

auto f4(std::vector<int>& vec)
{
    for (int& e : vec) {
        e = 0;
    }
    return vec;
}

解答・解説

問題2 (確認★)

次の関数には問題があります。問題がないように書き換えてください。

// 0 からはじまる連番値を生成する
// length: 生成される数値の列の長さ
// 戻り値: 生成された数値の列
std::vector<int>& generate_numbers(int length)
{
    std::vector<int> numbers {};
    for (int i = 0; i < length; ++i) {
        numbers.push_back(i);
    }
    return numbers;
}

解答・解説

問題3 (基本★)

std::vector<int> に、指定した値の要素が含まれているかどうかを調べて、結果を bool型の戻り値で返す contains関数を作成してください。

解答・解説

問題4 (基本★★)

std::vector<int> の要素のうち、偶数番目の要素(0,2,4,・・・) と、奇数番目の要素(1,3,5,・・・)の合計値をそれぞれ計算し、std::pair<int, int> で返す関数を作成してください。要素が不足している場合は 0 とします。

解答・解説

問題5 (応用★★)

蔵書リストのプログラムで、各コマンドの処理をそれぞれ別個の関数に切り分けてください。

現時点での蔵書リストのプログラムのソースコードは、前のページの練習問題の解答ページにあります。

解答・解説

問題6 (応用★★)

蔵書リストのプログラムには、標準入力から int型の整数を受け取る箇所がいくつかあります。この処理を関数に切り分けてください。

解答・解説


解答・解説ページの先頭



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る