C++OpenSiv3D入門講座 Vol. 07 remove_if・ラムダ式

C++OpenSiv3D入門講座

今回は前回とは別の、vectorの要素を削除する方法を学ぶ。演習問題では前回と同様敵を削除する。 vectorはコンテナの一種である。algorithmヘッダーはコンテナに対して削除・ソートなどを行う事ができる。 今回はコンテナに対し条件に一致する要素を削除する関数、remove_ifを使い、要素を削除する。また条件の指定にラムダ式を用いる。

要素を削除 std::remove_if

remove_ifは条件にあった要素を削除する。 正確には、条件にあった要素を、vectorの後ろに配置しなおし、削除したように見せかけた後のvectorの最後のイテレータを返す。 つまり、remove_if関数を呼んだだけでは要素は削除されておらず、その後にerase関数を呼ぶ必要がある。 remove_ifを使うには、algorithmヘッダーをインクルードする。

remove_ifの使い方

//remove_ifを実行し、条件にあった要素を後ろに詰める。
auto rmvIter = std::remove_if(vec.begin(),vec.end(), 関数オブジェクト);

//後ろの方に詰められた要素を削除
vec.erase( rmvIter, vec.end() );

remove_ifの第一引数には削除するかを判定する範囲の先頭、第二引数には終端を書く。 remove_ifの第三引数には削除の条件を返す関数を書く。

3の倍数を削除する例

#include <iostream>
#include <vector>
#include <algorithm>

//3の倍数かどうかをたしかめて返す叙述関数
bool isMultipleThree(int x){
    return (x%3) == 0;
}

int main(){

    std::vector<int> vec;//int型の動的配列

    //0から数を入れる
    for(int i = 0; i < 10; i++){
        vec.emplace_back(i);
    }

    //表示
    for(auto i = vec.begin(); i < vec.end(); ++i){
        std::cout << *i << " ";
    }
    std::cout << std::endl;


    //vecの中から3の倍数を後ろに詰める
    //3の倍数かどうかの判定は3番目のパラメータで渡された関数で判定
    auto rmvIter = std::remove_if(vec.begin(), vec.end(), isMultipleThree);

    //実際に削除
    vec.erase( rmvIter, vec.end() );

    //表示
    for(auto i = vec.begin(); i < vec.end(); ++i){
        std::cout << *i << " ";
    }
    std::cout << std::endl;

    return 0;
}

remove_ifの第三引数には、remove_ifが対象にするvectorの中身を引数に取り、bool型(trueかfalse)を返す関数を入れる。以下が例。

bool Func(配列の要素の型 x){
    return (x%3) == 0;//条件に一致してるか確認してbool型を返す
}

動作の様子を、以下の図に示す。 f:id:chinimuruhi:20211126162618p:plain

ラムダ式

本項目ではC++の中でも特に書き方が変態的なラムダ式を扱う。 remove_ifを使う時、大きなプログラムになると削除するかを判定する関数の定義と使う部分が離れてしまう。そこでC++11からはラムダ式というものが導入された。 例えば2で割り切れるかを返す関数オブジェクトを生成する場合

#include <iostream>

int main() {

    //2乗した数を返す関数オブジェクトを生成
    auto mySquare = [](int num) {return num * num; };

    std::cout << mySquare(3) << std::endl;
    std::cout << mySquare(4) << std::endl;

    return 0;

}

上のコードの

 [](int num) {return num * num; };

の部分がラムダ式だ。ラムダ式の型はautoで推論させている。(明示的に型指定をした std :: function オブジェクトや、一部関数ポインタに代入することもできるが、autoで受け取った方が良い)
ラムダ式の書き方(簡易版)

[](引数){処理};

ところで返り値の型を指定していないことに気づいただろうか? これは多くの場合指定しなくてよいものだ。 ラムダ式の返り値を明示的に指定する場合は

[](引数)->返り値の型{
    処理
};

のように()のあとに「->(返り値の型)」という形式にする。その他にも様々なオプションが存在するため、興味があれば調べてみよう。

ラムダ式の活用法

ラムダ式の節での処理は以下のように今まで通り関数を作って使っているのと同じようなものである。

#include <iostream>
//2乗した数を返す関数
auto mySquare(int num) {
    return num * num;
}

int main() {
    std::cout << mySquare(3) << std::endl;
    std::cout << mySquare(4) << std::endl;

    return 0;

}

ラムダ式は関数の一時オブジェクトを作成することができるのが強みの一つである。どのような場面でラムダ式を使うのが良いか、実際に例を挙げる。


関数の引数に関数オブジェクトを渡すときの例

以下は要素を削除 std::remove_ifの節での例の中で、remove_ifを呼んでいる箇所である。

auto rmvIter = std::remove_if(vec.begin(), vec.end(), isMultipleThree);

remove_ifの第三引数には、削除する対象の要素であるかどうかを返す関数を入れていた。

bool isMultipleThree(int x){
    return (x%3) == 0;
}

第三引数に渡す関数オブジェクトを、ラムダ式を用いて直接書くことができる。こうすることにより、remove_if関数の近くに条件を判定する関数を置くことができる。

#include <iostream>
#include <vector>
#include <algorithm>

int main(){

    std::vector<int> vec;//int型の動的配列

    for(int i = 0; i < 10; i++){
        vec.emplace_back(i);
    }

    //表示
    for(auto i = vec.begin(); i < vec.end(); ++i){
        std::cout << *i << " ";
    }
    std::cout << std::endl;




    //vecの中から3の倍数を後ろに詰める
    auto rmvIter = std::remove_if(vec.begin(),vec.end(),
        [](int x){
            return (x % 3) == 0;
        }
    );

    //実際に削除
    vec.erase( rmvIter, vec.end() );


    //表示
    for(auto i = vec.begin(); i < vec.end(); ++i){
        std::cout << *i << " ";
    }
    std::cout << std::endl;

    return 0;
}

std等に関数オブジェクトを引数にとる関数はいくつか存在する。( std:sort()std::foreachなど) 引数に渡すためなど一度きりしか使われなく、内容が複雑でない関数には積極的にラムダ式を使っていこう。

まとめ

  • vectorの条件に合った要素を削除するのにremove_ifが使える
  • remove_ifでは不要な要素を後ろに並び替えるだけであり、削除は行われていない
  • 関数の一時オブジェクトを作成するのにラムダ式が使える
  • 関数の引数に関数オブジェクトを渡す場合で、内容が複雑でない関数には積極的にラムダ式を使っていこう

演習問題(コンソール)

  1. int型の変数を引数にとり、2倍にして返す関数をラムダ式を用いて作成せよ。
  2. int型のvectorに0から9を入れ、remove_ifとラムダ式を用いて2の倍数を削除せよ。

演習問題(OpenSiv3D)

  1. 前回同様にEnemyクラスを用意した。Enemyのvectorを作り、敵を複数生成せよ。
    #pragma once
    #include <Siv3D.hpp>
    
    class Enemy {
    public:
        Vec2 pos;
        Vec2 velocity;
        Enemy(const Vec2& _pos);
        void update();
        void draw();
    };
    
    # include "Enemy.h"
    
    Enemy::Enemy(const Vec2& _pos) :
        pos{ _pos },
        velocity{ RandomVec2(50.0) }
    {
        // RandomVec2(double length)
        // 半径length(今回は50.0)の2次元ベクトルを返す
    }
    
    void Enemy::update() {
        pos += Scene::DeltaTime() * velocity;
    }
    
    void Enemy::draw() {
        Circle{ pos, 30.0 }.draw(Color{ 255, 0, 0 });
    }
    
  2. 画面外に出た敵を削除せよ。要素の削除にはalgorithmのremove_ifを使い、その条件の指定にはラムダ式を使用せよ。

C++OpenSiv3D入門講座