🦄

由緒正しきただ一つのreturn、はたして

に公開

恐らく皆様はもうご存じだとは思いますが、今日は「single exit point」と「early return」というプログラミングにおいてかなり基本的な概念に触れてみようと思います。

以下の関数のどちらの方が読みやすいと思いますか?

int singleExitPoint(int id)
{
    int ret = 0;
    if (id >= 0)
    {
        ret = doSomething(id);
        if (ret < 0)
        {
            std::cout << "doSomething failed with " << ret << std::endl;
        }
    }
    else
    {
        std::cout << "Invalid id given: " << id << std::endl;
        ret = INVALID_ID;
    }

    return ret;
}
int earlyReturn(int id)
{
    if (id < 0)
    {
        std::cout << "Invalid id given: " << id << std::endl;
        return -1;
    }

    const int ret = doSomething(id);
    if (ret < 0)
    {
        std::cout << "doSomething failed with " << ret << std::endl;
        return ret;
    }

    return OK;
}

前者の方がコードを書く時に凄く自然に感じると思います。条件をそのまま書いて、その条件が満たされている場合はdoSomethingという関数をコールして、満たされていない場合はエラーにする。returnする箇所が一カ所だけということで、こちらを「single exit point」って言います。
Cライクコードでは、一番最後にリソース開放とか後片付けなどをしないといけないため、大体こちらのパターンになりがちです。gotoを使って、その唯一の「exit point」に飛んだりします。

後者の方が条件を逆にしないといけないため、書く時に少し考え方を変えないといけないです。こういうエラーを先に片付けるコードを「early return」と言います。モダンC++ではリソース解放とか後処理はオブジェクトのデストラクタに任せるため、複数の「exit point」があっても問題ないです。
こういう簡単なケースでは、どちらも分かりやすいため、殆ど変わらない思います。

では、以下のもっと複雑な場合はどうでしょうか?

int singleExitPoint(int id, std::string name, int value)
{
    int ret = 0;
    if (id >= 0)
    {
        if (!name.empty())
        {
            if (value != 0)
            {
                ret = doSomething(id, name, value);
                if (ret < 0)
                {
                    std::cout << "doSomething failed with " << ret << std::endl;
                }
            }
            else
            {
                std::cout << "Invalid value given: " << value << std::endl;
                ret = -3;
            }
        }
        else
        {
            std::cout << "Invalid name given: " << name << std::endl;
            ret = -2;
        }
    }
    else
    {
        std::cout << "Invalid id given: " << id << std::endl;
        ret = -1;
    }

    return ret;
}
int earlyReturn(int id, std::string name, int value)
{
    if (id < 0)
    {
        std::cout << "Invalid id given: " << id << std::endl;
        return -1;
    }
    if (name.empty())
    {
        std::cout << "Invalid name given: " << name << std::endl;
        return -2;
    }
    if (value == 0)
    {
        std::cout << "Invalid value given: " << value << std::endl;
        return -3;
    }

    const int ret = doSomething(id, name, value);
    if (ret < 0)
    {
        std::cout << "doSomething failed with " << ret << std::endl;
        return ret;
    }

    return OK;
}

少し悪意がある例かもしれないですが、個人的にですが、複雑になればなるほど、「single exit point」はどんどん分岐が増えて読みづらくなると思います。デバッグする時もreturnが一番最後に来るため、どのケースでも関数の処理を最後まで追わないといけないです。
だからこそgotoを使うことで分岐を増やさないようにするのですが、gotoを使うと、関数の途中で変数を定義できなくなったり、関数の全体的なフローが複雑になるため、避けた方が良いです。
後、「single exit point」はretのような戻り値用の変数を使いまわしがちです。

early return」の場合は、エラーケースを関数の頭で1つずつ片付けることができるため、関数の頭のチェックが増えるだけで、コードはそんなに読みづらくならないです。デバッグする時も、エラーケースが追いやすいです。
後、「early return」はretを頭に定義する必要がなく、エラーを直接返せます。

最終的に好みの話になります。関数が複数の箇所からreturnしている方がお追いづらいと感じる人もいます。あなたならどちらを使いますか?


|cpp記事一覧へのリンク|

Discussion