61845


私は関数型プログラミングをしたことがなく、今、C++で関数型プログラミングを学ぼうとしています。（そのようなレベルの人が質問しているとお考えください。）

手始めに高階関数やカリー化の実装を見たとき、便利ではあるし、いくつかは既に（関数型プログラミングとは意識せずに）使っていると感じました。

一方で、これから学ぶであろう多くの関数型プログラミング技法をC++で使ったとして、果たしてコンパイラの支援を良く受けることができるのだろうか？という疑問が出てきました。

関数型プログラミング言語として設計されたコンパイラなら、それに基づく多くの制約によってより強い支援が受けられると感じます（そうでない言語（コンパイラ）ならばメリットの一部しか享受できないのではないかと）。

ご意見をお聞かせいただければと思います。


CC BY-SA 4.0
2019-12-27T08:17:56.413
61829
2023-05-07T04:30:27.387
2019-12-27T08:54:56.953
37013
37013
question
c++
C++での関数型プログラミングは良い結果をもたらしますか？


関数型プログラミングと言っても、定義は様々だと思いますが、関数を第一級オブジェクトとして扱うのは共通だと思います。C++でも、関数を第一級オブジェクトとして使って、分かりやすいコードを書くのは可能です。

Cに`qsort`という関数があります。これは、クイックソートのアルゴリズムのうち、値を比較する部分を抽象化しオブジェクトにして、いろいろなデータ型に使えるようにしようとしているのですが、Cでは関数はポインタで渡すしかないため、利用が非常に面倒でバグも出やすくなっています。C++では、ラムダ関数やテンプレートのおかげで、このような場合も見やすくバグの出にくいコードが書けるようになっています。

その一方で、多くの関数型言語では関数に副作用がなく、それを利用した最適化、たとえば遅延評価のようなことをしています。しかしC++では、実引数の評価は、関数に渡される前にされます。したがって、C++で関数型言語と全く同じようにコードを書いてしまうと、パフォーマンスの悪化や意図せぬ無限ループなどの結果になりかねません。もちろん、テンプレートやクラスを駆使して、自分で遅延評価のメカニズムを作ることはできますが、それはコンパイラによる関数型プログラミングのサポートとは、とても言えないでしょう。

関数型プログラミングのうち、C++に合う部分だけを取り入れるのが、現実的だと思います。


CC BY-SA 4.0
2019-12-27T18:55:58.717
61845
2019-12-27T18:55:58.717
3605
61829
answer


この回答にはやや専門用語が多く、関数型プログラミングに初めて入門する方向けのものではありません。しかし後からこの質問を見に来られた方のために回答として残しておくことに意義がありそうと思い、投稿します。

# 短い回答

C++
の関数型プログラミングの仕組みはどんどん整備されていっており、現状でも充分使えるレベルのものだと思います。関数型プログラミングのメリットを重要視するのであれば、まずは関数型プログラミングのやり方で書いてみて、そのあとパフォーマンスチューニングを行えば良いでしょう。

# 長い回答

まず「C++
で関数型プログラミングをする」という言い方には、いくつかの側面がありそうです。というのも、関数型プログラミングという言葉がどのような言語機能を指しているのかやや多面的だからです。たとえば「[『関数型言語』に関するFAQ形式の一般的説明](https://qiita.com/esumii/items/ec589d138e72e22ea97e)」という記事にはこう書かれています（抜粋しつつ引用します）：

> **関数型言語って何？**
>
> 関数型言語とは、厳密には関数型プログラミング言語(functional programming
> language)の略で、「関数型プログラミングを推奨・支援するプログラミング言語」のことです。
>
>
> そういう定義なので、「どの言語が関数型で、どれが関数型ではない」という明確な線引きは存在しません。「JavaやC++は関数型言語である」という人々もいます。「OCamlはC++より関数型言語っぽい」「HaskellはOCamlより関数型言語っぽい」など、「より関数型言語っぽい」という「感じ」は存在します。
>
> **じゃあ関数型プログラミングって何？**
>
> 副作用をできるだけ（あるいは全く）用いず、式や関数で計算を表すプログラミングスタイルのことです。

そして副作用をできるだけ用いないための書き方や機能がいくつか知られており、それらが大雑把に「関数型プログラミング」と呼ばれてしまっている印象です。

そこで、C++
においてどういう意味で関数型プログラミング的な機能を利用するのかという側面で切り分けつつ、コンパイラの支援が具体的にどうなるのか例を見てみたいと思います。

## fold/unfold

よく「関数型プログラミングっぽい」と言われる書き方に、fold や unfold があります。その特殊な形としてたとえば、リストの filter や map
を考えてみましょう。C++ ではこれらを使って書いてもコンパイラの恩恵を受けられます。

例として「与えられた `std::vector<int>` から 5 未満の要素を抜き出し 3
を足す」という操作を考えてみましょう。関数型プログラミング以前の書き方では、たとえば次のように書けます。

[code]

    // std::vector<int> v; が定義されているとして……
    
    std::vector<int> result;
    for (auto it = v.begin(), e = v.end(); it != e; it++) {
      if (*it < 5) {
        result.push_back((*it) + 3);
      }
    }
    
[/code]

あるいは C++11 の範囲 for 文を使うと次のように書けます。

[code]

    std::vector<int> result;
    for (const auto& e : v) {
      if (e < 5) {
        result.push_back(e + 3);
      }
    }
    
[/code]

これを C++11 の `std::copy_if` (filter 相当) や `std::transform` (map 相当)
を使って書くとこうなります。

[code]

    std::vector<int> result(v.size());
    std::copy_if(v.begin(), v.end(), std::back_inserter(result), [](int x) { return x < 5; });
    std::transform(result.begin(), result.end(), result.begin(), [](int x) { return x + 3; });
    
[/code]

これらのどれが一番速いかはなかなか判断が難しいですが、一番下のものでもコンパイラの恩恵を充分に受ける場合があるというのは断言できます。たとえば一番下のプログラムを
x86-64 clang 9.0.0 でコンパイルしたところ、SIMD 命令にしてくれていました：[godbolt
での結果](https://godbolt.org/z/JGG44V)。

一方で、他の関数型言語と同じくらい恩恵が受けられているのかはよく分かりません。たとえば Haskell
という関数型プログラミング言語は、副作用を上手く分離できているおかげで fold/unfold
まわりで[融合変換](https://stackoverflow.com/q/38905369/5989200)という最適化を行えるのですが、副作用も簡単に書ける
C++ でこれを行うのはすぐには難しそうです。ただこの最適化ができないからといって速度差にどの程度影響しているのかは判断しづらいです。

## 高階関数と第一級の関数

関数型というくらいですから、関数自体が値として扱える（関数が第一級オブジェクトである）ことは大事でしょう。また、関数を引数として受け取るような関数（高階関数）も関数型プログラミングで良く出てくる概念です。実際、先に述べた
fold/unfold は高階関数です。

C++ では C++11 あたりから `std::function` やラムダ式が整備され、使いやすくなりました。

一方で `std::function`
はゼロコストな機能ではありません。オーバーヘッドがあります。しかし実装やコンパイラのおかげでそのオーバーヘッドは充分小さく、むしろ
`std::function` を使ってプログラムが整理されるメリットの方が大きいのでは、という風潮があります。たとえば ["What is the
performance overhead of
std::function?"](https://stackoverflow.com/q/5057382/5989200) をご覧ください。

また、`std::function` はそのコピーコストが問題視される場合があるのですが、じゃあユースケースを限定すればコピーしない形にできるだろうと言って
`std::function_ref` というものが提案されていたりもします（参考：[C++
proposalとして提案中のfunction_refを実装してみた](https://qiita.com/Kogia_sima/items/1c0f93fd1350d64e529a)）。これはよくある関数型プログラミングの機能を
C++ っぽく使うために改造していく例のひとつと言えそうです。この意味で、C++ で行う関数型プログラミングにも一定の意味があると言えるでしょう。

## イミュータブルな変数

「変数が書き換わる」という挙動は副作用として捉えることができます。このため関数型プログラミング言語では変数が標準でイミュータブルなことが多いです。C++
はそうではありません。

C++ でイミュータブルな変数というと、似た役割を持つ機能として `const` が思いつきます。そして `const` 自体は C++
に昔からある機能であり、これはコンパイラの恩恵を充分に受けられるといって良いでしょう。

ただし C++ ではメモリ管理を自分で行う都合上、どこまで `const`
にすべきか、どちらの方が性能が良いかは慎重に選ぶべきと思います。たとえば最初の方にある `std::vector`
を操作する例では、`std::transform` する際にいちいち新しく `std::vector`
用のメモリをアロケートしていると性能が落ちてしまいます。

## 代数的データ型とパターンマッチ

関数型プログラミング言語によく備わっている言語機能として、代数的データ型があります。これは別に必須の機能というわけではないのですが、多いので紹介します。

C++14 までは、C++ に代数的データ型そのものは存在しませんでした。代数的データ型が欲しくなる場面では、union
などを使って処理していたでしょう。C++17 では `std::variant` や `std::optional` が導入されました。

（※ `std::variant` に対するコンパイラ最適化が現状どのくらい効いているのか、私は詳しくありません。調査が必要です。一例として
[godbolt で試してみた例をリンクします](https://godbolt.org/z/TbuJrn)。）

パターンマッチについては C++17 時点ではまだ導入されておらず、協議中です。`std::variant` では代わりに `std::visit`
がありますが、パターンマッチほど便利ではありません。

## 補足1：遅延評価

関数型プログラミング言語として有名な言語のひとつに、Haskell があります。この言語は **遅延評価**
という仕組みを採用しており、それゆえ関数型プログラミングの特徴のひとつとして遅延評価が挙がる場合があります。しかし、本来は遅延評価かどうかは関数型プログラミングとはあまり関係がありません。たとえば同じく関数型プログラミング言語として有名な
OCaml はデフォルトでは遅延評価になりません。

## 補足2：コンパイラの性能向上

この回答は 2020 年 2
月現在リリースされているコンパイラを元に書かれました。しかし、コンパイラというものはバージョンアップしていくものです。もし関数型プログラミングの技法が今後の
C++ でもっと多用されるようになれば、それに合わせてコンパイラもある程度更に最適化を進めるでしょう。

また、任意のプログラムをどのくらい最適化できるかという話と、コンパイラを意識して書かれたプログラムをどのくらい最適化できるかという話は別です。関数型プログラミングっぽく書きつつパフォーマンスも上げる書き方が今後ますます発見されていくのではないかと思います。
**最初はとりあえず一通り書いてみて、求めていた性能が得られなければボトルネックを他のやり方で書きなおす**
、というのはパフォーマンスチューニングで良くある方法であり、今回の例でも同じように、まずは普通の関数型プログラミングのやり方で書いて不便しないのではないかと思います。少なくとも最初から無茶苦茶困るようなオーバーヘッドは無いことは、この回答で述べたとおりです。

関数型プログラミングのメリットのひとつは、副作用がたくさん使われている書き方と比べてテストや検証がしやすいという点です。このメリットと性能をどう天秤にかけるかはプロジェクトによります（たとえば極端に言えばアセンブリで書くのが一番速いですが、そうしないのは何かしらのメリットがあるからですよね、それと似ています）。長所短所を見測るためのひとつの手がかりとして、この回答が役に立てば幸いです。


CC BY-SA 4.0
2020-02-22T06:56:59.473
63263
2020-02-22T10:45:35.670
2020-02-22T10:45:35.670
19110
19110
61829
answer


C++20で[範囲ライブラリ](https://ja.cppreference.com/w/cpp/ranges)が導入されます。これを使うとnekketsuuuさんの

> 例として「与えられた `std::vector<int>` から 5 未満の要素を抜き出し 3 を足す」という操作を考えてみましょう。

は次のように書けます。 [wandbox](https://wandbox.org/permlink/tGwkgZyH3ukKVNOH)

[code]

    std::vector<int> v{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    auto u = v
        | ranges::view::filter([](auto x) { return x < 5; })
        | ranges::view::transform([](auto x) { return x + 3; })
        | ranges::to<std::vector<int>>;
    
[/code]

C++言語でも関数型プログラミングのような記述ができるようになります。

* * *

C++23
で[`std::ranges::to`](https://en.cppreference.com/w/cpp/ranges/to)が導入されるので実装の早いコンパイラーであれば現時点でも

[code]

    std::vector v{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    auto u = v
        | std::ranges::views::filter([](auto x) { return x < 5; })
        | std::ranges::views::transform([](auto x) { return x + 3; })
        | std::ranges::to<std::vector>();
    
[/code]

と書けるようになっています。


CC BY-SA 4.0
2020-02-22T08:04:02.877
63267
2023-05-07T04:30:27.387
2023-05-07T04:30:27.387
4236
4236
61829
answer