🖥️

【C++言語入門】 第25回 クラス中のラムダ式と引数

に公開

https://youtu.be/o2Jqg5_jrG8

四国めたん
\textcolor{pink}{四国めたん: }教師役ですわ

ずんだもん
\textcolor{lime}{ずんだもん: }生徒役なのだ

\footnotesize \textcolor{pink}{四国めたん:} こんにちは。四国めたんです

\footnotesize \textcolor{lime}{ずんだもん:} ずんだもんなのだ。こんにちはなのだ

\footnotesize \textcolor{pink}{四国めたん:} 今回は関数の引数として関数や ラムダ式 を使う方法についてお話しをしていきますわ

\footnotesize \textcolor{lime}{ずんだもん:} 関数や ラムダ式 を関数の引数として使うことができるのか?

\footnotesize \textcolor{pink}{四国めたん:} 可能ですわね

\footnotesize \textcolor{lime}{ずんだもん:} ふむ

\footnotesize \textcolor{pink}{四国めたん:} さらにクラス中で ラムダ式 を使う方法についてお話しをしていきますわ

\footnotesize \textcolor{lime}{ずんだもん:} よろしくお願いなのだ

関数の引数に関数を

\footnotesize \textcolor{pink}{四国めたん:} さて、前回は関数や ラムダ式 を変数に割り当てる方法を説明しましたわ

\footnotesize \textcolor{lime}{ずんだもん:} 覚えているのだ

\footnotesize \textcolor{lime}{ずんだもん:} でも、変数に割り当てることができるのならば、関数の引数に割り当てることも可能なのではないのか?

\footnotesize \textcolor{pink}{四国めたん:} 鋭いですわね

\footnotesize \textcolor{pink}{四国めたん:} 関数の引数に、関数や ラムダ式 を割り当てることは、結構、普通に行われますわ

\footnotesize \textcolor{lime}{ずんだもん:} そうなのか?

\footnotesize \textcolor{pink}{四国めたん:} はい、ソートの関数などは代表例ですわ

\footnotesize \textcolor{lime}{ずんだもん:} ソートの関数?

\footnotesize \textcolor{pink}{四国めたん:} 例えば、C言語ではqsort、C++言語ではstd::sortなどでしょうか?

\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

関数を引数に代入しよう

\footnotesize \textcolor{pink}{四国めたん:} まずは一般の関数を関数の引数に割り当ててみましょう

\footnotesize \textcolor{lime}{ずんだもん:} よろしくなのだ

\footnotesize \textcolor{pink}{四国めたん:} 形式の一例としては以下の通りです

戻り値の型 関数名(関数の戻り値の型 (*引数名)(引数...), ...) { 処理 };

\footnotesize \textcolor{lime}{ずんだもん:} 結構、複雑なのだ

\footnotesize \textcolor{pink}{四国めたん:} はい、関数の引数部分に、前回お話しした変数の形式を当てはめた形となりますわ

\footnotesize \textcolor{lime}{ずんだもん:} 関数の戻り値の型 (*引数名)(引数...)の部分が引数なのか...

\footnotesize \textcolor{pink}{四国めたん:} とりあえずプログラムを見てみましょう

#include <iostream>

int Add(int a, int b) {
  int c = a + b;
  return c;
}

void Message(int (*add)(int, int), const char* pmsg, int a, int b) {
  std::cout << pmsg << add(a, b) << std::endl;
  return;
}

int main(int argc, char* argv[]) {
  Message(Add, "一般の関数の足し算 ", 1, 2);
  Message([](int x, int y) -> int { return x + y; }, "ラムダ式の足し算 ", 3, 4);
  return 0;
}

関数を引数に

\footnotesize \textcolor{lime}{ずんだもん:} Message関数の引数としてAdd関数を割り当てているのか?

\footnotesize \textcolor{pink}{四国めたん:} はい、追加で キャプチャリスト のない ラムダ式 も割り当ててみましたわ

\footnotesize \textcolor{lime}{ずんだもん:} つまり、今回の場合は、戻り値がintで、int型の引数を2つ持っている関数や ラムダ式 であればOKということか

\footnotesize \textcolor{pink}{四国めたん:} その通りですわ

キャプチャリストを持つラムダ式を渡す

\footnotesize \textcolor{lime}{ずんだもん:} キャプチャリスト を持つ ラムダ式 を渡す場合にはどうすればいいのだ?

\footnotesize \textcolor{pink}{四国めたん:} それをお話しする前に、Message関数の引数に キャプチャリスト を持つ ラムダ式 を渡してみましょう

#include <iostream>

void Message(int (*add)(), const char* pmsg) {
  std::cout << pmsg << add() << std::endl;
  return;
}

int main(int argc, char* argv[]) {
  int x = 1;
  int y = 2;
  Message([=]() -> int { return x + y; }, "ラムダ式の足し算 ");
  return 0;
}

ラムダ式のエラー

\footnotesize \textcolor{lime}{ずんだもん:} 「'main::<lambda_1>'から'int (__cdecl *)(void)'へ変換できません。」と云うエラーが出ているのだ

\footnotesize \textcolor{pink}{四国めたん:} そうですわね

\footnotesize \textcolor{pink}{四国めたん:} そこで、変数の場合と同様に、型としてstd::function<>を使ってみますわ

\footnotesize \textcolor{lime}{ずんだもん:} うむ

\footnotesize \textcolor{pink}{四国めたん:} 形式の一例としては以下の通りですわ

戻り値の型 関数名(std::function<関数の戻り値の型(引数...)> 引数名, ...) { 処理 };

\footnotesize \textcolor{lime}{ずんだもん:} これもかなり複雑なのだ

\footnotesize \textcolor{pink}{四国めたん:} たしかに複雑ですが、関数の引数部分に変数の形式を当てはめた形となりますわ

\footnotesize \textcolor{lime}{ずんだもん:} std::function<関数の戻り値の型(引数...)> 引数名の部分が、そのまま引数となるのだ

\footnotesize \textcolor{pink}{四国めたん:} その通りですわ

\footnotesize \textcolor{pink}{四国めたん:} とりあえずプログラムを見てみましょう

#include <iostream>
#include <functional>

void Message(std::function<int()> add, const char* pmsg) {
  std::cout << pmsg << add() << std::endl;
  return;
}

int main(int argc, char* argv[]) {
  int x = 1;
  int y = 2;
  Message([=]() -> int { return x + y; }, "ラムダ式の足し算 ");
  return 0;
}

std::finction

\footnotesize \textcolor{lime}{ずんだもん:} 問題なく実行できたのだ

\footnotesize \textcolor{pink}{四国めたん:} ちなみに#include <functional>は忘れずに記述して下さいね

\footnotesize \textcolor{lime}{ずんだもん:} わかったのだ

\footnotesize \textcolor{pink}{四国めたん:} なお、std::function<>であれば、 ラムダ式 だけではなく、通常の関数を割り当てることもできますわ

\footnotesize \textcolor{lime}{ずんだもん:} それは便利なのだ

引数の型にauto

\footnotesize \textcolor{lime}{ずんだもん:} ところで、関数の引数にstd::function<>を使用できるなら、autoによる 型の推論 も可能ではないのか?

\footnotesize \textcolor{pink}{四国めたん:} そうですわね

\footnotesize \textcolor{pink}{四国めたん:} でも、引数には型を推測するためのヒントがないので、autoによる 型の推論 はできないようですわ

\footnotesize \textcolor{lime}{ずんだもん:} そうなのか...残念なのだ

\footnotesize \textcolor{pink}{四国めたん:} 関数の戻り値をautoにすることはできるようですが...

\footnotesize \textcolor{lime}{ずんだもん:} その辺りを詳しく教えて欲しいのだ

\footnotesize \textcolor{pink}{四国めたん:} 申し訳ないのですが、関数や ラムダ式 を関数の戻り値にするのは、ほとんど需要がありません

\footnotesize \textcolor{lime}{ずんだもん:} そうなのか?

\footnotesize \textcolor{pink}{四国めたん:} また、色々と問題を引き起こすのでお勧めしませんわ

\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

\footnotesize \textcolor{pink}{四国めたん:} というわけで、説明は省かせていただきますわ

\footnotesize \textcolor{lime}{ずんだもん:} むぅ~

関数の戻り値に関数?

関数の引数に関数や ラムダ式 を使えるのであれば、戻り値にも関数や ラムダ式 を使えると考えるのは当然です

できます

関数の戻り値に、変数や引数と同様の型指定を行えばOKです

例えばこんな感じです

#include <iostream>
#include <functional>

std::function<int(int, int)> SelectFunc(int s) {
  int x = 2;
  std::function<int(int, int)> f;
  if (s == 0) {
    f = [](int a, int b) -> int { return a + b; };         // (1)
  } else {
    f = [=](int a, int b) -> int { return (a + b) * x; };  // (2)
  }
  return f;
}

int main(int argc, char* argv[]) {
  auto func0 = SelectFunc(0);
  auto func1 = SelectFunc(1);
  std::cout << func0(1, 2) << std::endl;
  std::cout << func1(1, 2) << std::endl;
  return 0;
}

戻り値にラムダ式

SelectFuncは、指定した引数により、ただの足し算の ラムダ式 (1)か、足した後に2倍する ラムダ式 (2)を返します

返された関数はメイン関数内で実行されます

さて、ここで問題になるのが(2)の キャプチャリスト です

上記の例では コピー "="を指定しているため、SelectFunc関数を抜けて"x"が破棄されても問題ありません

ここで(2)の キャプチャリスト を参照"&"にしてみましょう

#include <iostream>
#include <functional>

std::function<int(int, int)> SelectFunc(int s) {
  int x = 2;
  std::function<int(int, int)> f;
  if (s == 0) {
    f = [](int a, int b) -> int { return a + b; };         // (1)
  } else {
    f = [&](int a, int b) -> int { return (a + b) * x; };  // (2)
  }
  return f;
}

int main(int argc, char* argv[]) {
  auto func0 = SelectFunc(0);
  auto func1 = SelectFunc(1);
  std::cout << func0(1, 2) << std::endl;
  std::cout << func1(1, 2) << std::endl;
  return 0;
}

戻り値にラムダ式でエラー

メイン関数でfunc1を実行する時点でSelectFunc関数を抜けているので、参照している"x"は既に破棄されています

結果として、意図した値とは異なる結果が表示されます

まぁ、こういった落とし穴があるので、関数や ラムダ式 を返す関数は、かなり慎重に作る必要があります

と云うか、経験上、関数や ラムダ式 を返す関数の需要は殆どありません

もしも、関数や ラムダ式 を返す関数が必要になった場合は、アルゴリズムを考え直した方が良いです

クラスの中のラムダ式

\footnotesize \textcolor{pink}{四国めたん:} 今まで一般の関数の中で ラムダ式 を使ってきましたわ

\footnotesize \textcolor{lime}{ずんだもん:} たしかにその通りなのだ

\footnotesize \textcolor{pink}{四国めたん:} 次はクラスのメソッドの中で ラムダ式 を使う場合についてお話ししますわ

\footnotesize \textcolor{lime}{ずんだもん:} よろしくなのだ

\footnotesize \textcolor{pink}{四国めたん:} まぁ、メソッドも関数ではあるので、今までお話しした ラムダ式 の内容については、基本的に当てはまりますわ

\footnotesize \textcolor{lime}{ずんだもん:} うむ

\footnotesize \textcolor{pink}{四国めたん:} 問題は、メソッドの特徴である、クラスの メンバ変数他のメソッド へのアクセスですわ

\footnotesize \textcolor{lime}{ずんだもん:} メソッド内で記述した ラムダ式 内から、クラスの メンバ変数他のメソッド には、普通にアクセスできるのではないのか?

\footnotesize \textcolor{pink}{四国めたん:} メソッドとは異なり、 ラムダ式 内から直接 メンバ変数他のメソッド にアクセスはできませんわね

\footnotesize \textcolor{lime}{ずんだもん:} ふ~ん、そうなのか

\footnotesize \textcolor{pink}{四国めたん:} また、 メンバ変数他のメソッド などは、メソッド内で宣言された変数ではないので、 キャプチャリスト に記載してもアクセスできませんわ

\footnotesize \textcolor{lime}{ずんだもん:} そうなのか?

\footnotesize \textcolor{pink}{四国めたん:} はい、記載してもエラーとなるだけですわ

クラス中のラムダ式からthisを通してアクセス

\footnotesize \textcolor{pink}{四国めたん:} ただし、例外がありますわ

\footnotesize \textcolor{lime}{ずんだもん:} ふむふむ

\footnotesize \textcolor{pink}{四国めたん:} インスタンス自身へのポインタであるthisは、 キャプチャリスト に記載することで ラムダ式 内で使うことができるようになりますわ

\footnotesize \textcolor{lime}{ずんだもん:} お~、それはうれしいのだ

\footnotesize \textcolor{pink}{四国めたん:} ポインタなので、thisを通して メンバ変数他のメソッド にアクセスすることができますわ

\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

\footnotesize \textcolor{pink}{四国めたん:} なお、 ラムダ式 自身がクラス内で定義されていますので、 メンバ変数他のメソッドpublic:以外でアクセス指定されていても問題なくアクセスできますわ

\footnotesize \textcolor{lime}{ずんだもん:} かなり便利なのだ

\footnotesize \textcolor{pink}{四国めたん:} とりあえずMessageメソッドを持ったCircleクラスで、Areaメソッドを ラムダ式 に書き換えてみましょう

circle.h
#pragma once

#ifndef CIRCLE_H
#define CIRCLE_H

#include <iostream>

const double kPI = 3.14159265358979323846;
const int kSize = 50;

/// @brief 円
class Circle {
  double diameter_;  // 直径
  char* pmessage_;

 public:
  Circle(double diameter) : diameter_(diameter), pmessage_(nullptr) {
    pmessage_ = new char[kSize];
  }
  virtual ~Circle() {
    delete[] pmessage_;
    pmessage_ = nullptr;
  }

  double Diameter() const { return diameter_; }
  void Diameter(double diameter) { diameter_ = diameter; }

  virtual const char* Message() const noexcept {
    auto area = [this]() {
      double radius = this->Diameter() / 2;
      double a = radius * radius * kPI;
      return a;
    };
    sprintf_s(pmessage_, kSize, "円の面積は%fです。", area());
    return pmessage_;
  }
};

#endif  // CIRCLE_H
hello_world.cpp
#include <iostream>
#include "circle.h"

int main(int argc, char* argv[]) {
  Circle c(10.0);
  std::cout << c.Message() << std::endl;
  return 0;
}

クラスでラムダ式

\footnotesize \textcolor{pink}{四国めたん:} 今回はthis->Diameter()の部分でthisを通してDiameterメソッドを呼び出していますわ

\footnotesize \textcolor{lime}{ずんだもん:} うむ

\footnotesize \textcolor{pink}{四国めたん:} なお、あまりお勧めはできませんが、this->diameter_として、直接 メンバ変数 にアクセスすることもできますわ

\footnotesize \textcolor{lime}{ずんだもん:} なるほどなのだ

\footnotesize \textcolor{pink}{四国めたん:} ちなみにthis->を省略することも可能ですわ

\footnotesize \textcolor{lime}{ずんだもん:} そうなのか?

\footnotesize \textcolor{pink}{四国めたん:} ちなみに キャプチャリストデフォルトコピーキャプチャ "="や デフォルト参照キャプチャ "&"を使ってもthisを通して メンバ変数他のメソッド にアクセスすることができますわ

auto area = [=]() {
  double radius = this->Diameter() / 2;
  double a = radius * radius * kPI;
  return a;
};

\footnotesize \textcolor{lime}{ずんだもん:} りょうかいなのだ

クラス中のラムダ式からthisを通して代入

\footnotesize \textcolor{lime}{ずんだもん:} ところで、thisを通して ラムダ式 の中から メンバ変数 の値を変更できるのか?

\footnotesize \textcolor{pink}{四国めたん:} 可能ですわ

\footnotesize \textcolor{pink}{四国めたん:} ただ、プログラム例ではMessageメソッドにconst修飾子が付加されているため、そのままでは メンバ変数 を変更できませんわ

\footnotesize \textcolor{lime}{ずんだもん:} つまりconst修飾子がなければ問題なく変更できるということか

\footnotesize \textcolor{pink}{四国めたん:} はい、とりあえずMessageメソッドからconst修飾子を外して、 ラムダ式 内から"diameter_"を変更してみましょう

circle.h
#pragma once

#ifndef CIRCLE_H
#define CIRCLE_H

#include <iostream>

const double kPI = 3.14159265358979323846;
const int kSize = 50;

/// @brief 円
class Circle {
  double diameter_;  // 直径
  char* pmessage_;

 public:
  Circle(double diameter) : diameter_(diameter), pmessage_(nullptr) {
    pmessage_ = new char[kSize];
  }
  virtual ~Circle() {
    delete[] pmessage_;
    pmessage_ = nullptr;
  }

  double Diameter() const { return diameter_; }
  void Diameter(double diameter) { diameter_ = diameter; }

  virtual const char* Message() noexcept {
    auto area = [this]() {
      this->Diameter(5.0);
      double radius = this->Diameter() / 2;
      double a = radius * radius * kPI;
      return a;
    };
    sprintf_s(pmessage_, kSize, "円の面積は%fです。", area());
    return pmessage_;
  }
};

#endif  // CIRCLE_H

メンバ変数の変更

\footnotesize \textcolor{pink}{四国めたん:} ラムダ式 内でthis->Diameter(5.0);により、直径を半分にしていますわ

\footnotesize \textcolor{lime}{ずんだもん:} エラーの発生もなく、普通に実行できているのだ

メンバ変数にラムダ式

ところで、通常の変数に ラムダ式 を割り当てられるのであれば、 メンバ変数 にも ラムダ式 を割り当てることができるのではないでしょうか

とりあえず確認してみましょう

circle.h
#pragma once

#ifndef CIRCLE_H
#define CIRCLE_H

#include <iostream>

#include <functional>

const double kPI = 3.14159265358979323846;
const int kSize = 50;

/// @brief 円
class Circle {
  double diameter_;  // 直径
  char* pmessage_;
  std::function<double()> area_ = [this]() {
    double radius = this->Diameter() / 2;
    double a = radius * radius * kPI;
    return a;
  };

 public:
  Circle(double diameter) : diameter_(diameter), pmessage_(nullptr) {
    pmessage_ = new char[kSize];
  }
  virtual ~Circle() {
    delete[] pmessage_;
    pmessage_ = nullptr;
  }

  double Diameter() const { return diameter_; }
  void Diameter(double diameter) { diameter_ = diameter; }

  virtual const char* Message() noexcept {
    sprintf_s(pmessage_, kSize, "円の面積は%fです。", area_());
    return pmessage_;
  }
};

#endif  // CIRCLE_H

メンバ変数にラムダ式

メンバ変数 area_ラムダ式 を指定しています

型はautoを使うことができないので、std::function<>を使っています

なお、 キャプチャリスト にはthis以外は デフォルトコピーキャプチャ "="や デフォルト参照キャプチャ "&"以外は使うことができないようです

使用時にメソッドから変数などから値を渡したい場合には、引数を使用します

とはいえ、メソッドでも概ね同じことが可能です

無理をして メンバ変数ラムダ式 を割り当てる必要はありません

まとめ

\footnotesize \textcolor{pink}{四国めたん:} お疲れ様ですわ

\footnotesize \textcolor{lime}{ずんだもん:} おつかれさまなのだ

\footnotesize \textcolor{pink}{四国めたん:} 以上で クラス中のラムダ式と引数 について終了しますわ

Discussion