C++コーディング演習(第1問~第8問)

PG

最近の休日のルーティン:

  • MusescoreのソースコードをChatGPT(MEMちょ似 25歳女性ソフトウェアエンジニアの設定)と二人三脚で追う
  • その日の学びを復習できるコーディング問題を彼女に出題してもらい、私が解いてフィードバックを受ける

一度解いても忘れるので自分用にまとめ。

注:下記の内容はChatGPTとの壁打ちをまとめているので、誤った情報が紛れている可能性がある。

  1. 第1問 インターフェース、仮想関数、コンストラクタ、デストラクタ (2/27)
    1. 問題:動物 (Animal) のクラス構造を作り、次の仕様を満たすプログラムを作成
    2. 最終解答
    3. 最終解答(2周目)
    4. ふりかえり
    5. ふりかえり (2周目、4/6)
  2. 第2問 インターフェース、ポリモーフィズム、シングルトン (2/28)
    1. 問題:図形(Shape)を扱うインターフェースとシングルトンのマネージャを実装
    2. 最終解答
    3. 最終解答(2周目)
    4. ふりかえり
    5. ふりかえり (2周目、4/6)
  3. 第3問 ディスパッチャー (3/2)
    1. 問題:「ディスパッチャーにアクションを登録して実行する流れ」を小さく再現
    2. 最終解答
    3. 最終解答(2周目)
    4. ふりかえり
    5. ふりかえり (2周目、4/5)
  4. 第4問 enum(列挙型)、リスト、条件分岐 (3/6)
    1. 問題:音符・休符リストの選択範囲を拡張するプログラム
    2. 最終解答
    3. 最終解答(2周目)
    4. ふりかえり
    5. ふりかえり (2周目、4/5)
  5. 第5問 ポインタ、参照 (3/7)
    1. 問題:動的配列を拡張する関数を実装
    2. 最終解答
    3. 最終解答(2周目)
    4. ふりかえり
    5. ふりかえり (2周目、4/3)
  6. 第6問 ベクター、条件分岐、ループの処理 (3/10)
    1. 問題:音符リストのナビゲーションを実装
    2. 最終解答
    3. 最終解答(2周目)
    4. ふりかえり
    5. ふりかえり (2周目、3/31)
  7. 第7問 フィルタ、ソート (3/16)
    1. 問題:有効/無効を持つ譜面セグメントをフィルタ&ソートして出力
    2. 最終解答
    3. 最終解答(2周目)
    4. ふりかえり
    5. ふりかえり (2周目、3/31)
  8. 第8問 座標変換 (3/17)
    1. 問題:二次元座標の変換プログラム
    2. 最終解答
    3. 最終解答(2周目)
    4. ふりかえり
    5. ふりかえり (2周目、3/31)
  9. まとめ

第1問 インターフェース、仮想関数、コンストラクタ、デストラクタ (2/27)

問題:動物 (Animal) のクラス構造を作り、次の仕様を満たすプログラムを作成

条件

  1. IAnimal インターフェースを作る
    • makeSound()(鳴き声を出す関数)を 純粋仮想関数 (= 0;) にする
    • 仮想デストラクタ (virtual ~IAnimal()) を持たせる
  2. Dog, Cat, Bird の3つのクラスを IAnimal を継承して作る
    • makeSound() をオーバーライドして、それぞれの鳴き声を出す ("Woof!", "Meow!", "Chirp!")
  3. std::vector<std::shared_ptr<IAnimal>> を使って動物リストを作る
    • Dog, Cat, Birdstd::make_shared<> で作成してリストに追加する
  4. リストの全ての動物について makeSound() を呼び出し、それぞれの鳴き声を出す

解答の形

IAnimal のインターフェースを作り、Dog, Cat, Bird のクラスを作成し、それらをリストで管理する!

#include <iostream>
#include <vector>
#include <memory>

// ここに `IAnimal` の定義を書く

// ここに `Dog`, `Cat`, `Bird` の定義を書く

int main() {
std::vector<std::shared_ptr<IAnimal>> animals;

// ここに `Dog`, `Cat`, `Bird` をリストに追加するコードを書く

// 全ての動物について `makeSound()` を呼ぶ
for (const auto& animal : animals) {
animal->makeSound();
}

return 0;
}

この問題を解けば、今日の学びをフル活用できるよ!

最終解答

#include <iostream>
#include <vector>
#include <memory>

// ここに `IAnimal` の定義を書く
class IAnimal {
public:
    virtual void makeSound() = 0;
//    IAnimal() {std::cout << "IAnimalコンストラクタ" << std::endl; }
//    virtual ~IAnimal() {std::cout << "IAnimalデストラクタ" << std::endl; };
};

// ここに `Dog`, `Cat`, `Bird` の定義を書く
class Dog : public IAnimal{
public:
//    Dog() {std::cout << "Dogコンストラクタ" << std::endl; }
//   ~Dog() {std::cout << "Dogデストラクタ" << std::endl; }

    void makeSound() override { std::cout << "Woof!" << std::endl; }
};

class Cat : public IAnimal{
public:
//    Cat() {std::cout << "Catコンストラクタ" << std::endl; }
//    ~Cat() {std::cout << "Catデストラクタ" << std::endl; }
    
    void makeSound() override { std::cout << "Meow!" << std::endl; }
};

class Bird : public IAnimal{
public:
//    Bird() {std::cout << "Birdコンストラクタ" << std::endl; }
//    ~Bird() {std::cout << "Birdデストラクタ" << std::endl; }
    
    void makeSound() override { std::cout << "Chirp!" << std::endl; }
};

int main() {
    std::vector<std::shared_ptr<IAnimal>> animals;
    std::cout << &animals << std::endl;

    // ここに `Dog`, `Cat`, `Bird` をリストに追加するコードを書く
    animals.push_back(std::make_shared<Dog>());
    animals.push_back(std::make_shared<Cat>());
    animals.push_back(std::make_shared<Bird>());
    
    std::cout << &animals << std::endl;

    // 全ての動物について `makeSound()` を呼ぶ
    for (const auto& animal : animals) {
        std::cout << "animal at:" << animal << std::endl;
        animal->makeSound();
    }

    return 0;
}

最終解答(2周目)

#include <iostream>
#include <vector>
#include <memory>

// ここに `IAnimal` の定義を書く
class IAnimal{
public:
    virtual void makeSound() = 0;
    virtual ~IAnimal() = default;
};

// ここに `Dog`, `Cat`, `Bird` の定義を書く
class Dog : public IAnimal{
public:
    void makeSound() override {
        std::cout << "Woof!" << std::endl;
    }
};

class Cat : public IAnimal{
public:
    void makeSound() override {
        std::cout << "Meow!" << std::endl;
    }
};

class Bird : public IAnimal{
public:
    void makeSound() override {
        std::cout << "Chirp!" << std::endl;
    }
};

int main() {
    std::vector<std::shared_ptr<IAnimal>> animals;

    // ここに `Dog`, `Cat`, `Bird` をリストに追加するコードを書く
    animals.push_back(std::make_shared<Dog>());
    animals.push_back(std::make_shared<Cat>());
    animals.push_back(std::make_shared<Bird>());

    // 全ての動物について `makeSound()` を呼ぶ
    for (const auto& animal : animals) {
        animal->makeSound();
    }

    return 0;
}

ふりかえり

  • override, const の明示をするとなおよい
  • ; のつけ忘れ、publicPublicと書き間違える
  • publicをかかないとデフォルトprivate →main()からインスタンスを作れない
  • デストラクタの { ...(処理)... }=default; は同時に書けない
    • いま(この記事執筆時)見返すと「それはそう」なのだが、これがはじめて書いたC++プログラムだったので…。今後もしばらくそういう初歩的なミスが続く。

ふりかえり (2周目、4/6)

  • std::shared_ptr<Dog> dog = std::make_shared<Dog>();
    animals.push_back(dog);

    の二段構えにするとよいのは次のとき
    • あとで同じインスタンスを使い回したい
    • デバッグでみたい、ブレークポイントを貼りたい
    • 処理を挟みたい
  • std::make_shared<Cat>;はエラー
    • make_sharedというテンプレート関数をCat型用に用意した、の意味になる
    • ()までないと何も起きないしエラーになる
  • animals.push_back({std::make_shared<Dog>(), std::make_shared<Cat>(), std::make_shared<Bird>() }); はダメなのか?
    • animalsstd::shared_ptr<IAnimal>型の値を待っているのに、3つ入りのリストを渡してしまっているからエラー
    • まとめて追加するなら、
      • 宣言と一緒に、vector<T> v = {a, b, c};
      • あるいは、 v.insert(v.end(), {a, b, c};

第2問 インターフェース、ポリモーフィズム、シングルトン (2/28)

問題:図形(Shape)を扱うインターフェースとシングルトンのマネージャを実装

問題内容

  1. IShape インターフェースを作る
    • 純粋仮想関数 draw() を定義(図形を描画するメソッド。コンソールにメッセージを表示するだけでOK)
    • 純粋仮想関数 area() を定義(図形の面積を返す。double 型)
  2. 以下の図形クラスを IShape を継承して作る
    • Circle
      • 半径 (radius) を持つ
      • draw()"Drawing Circle (半径は...) " と出力
      • area()π * 半径^2
    • Rectangle
      • 幅 (width), 高さ (height) を持つ
      • draw()"Drawing Rectangle (幅x高さ...)" と出力
      • area()width * height
    • Triangle
      • 底辺 (base), 高さ (height) を持つ
      • draw()"Drawing Triangle (底辺x高さ...)" と出力
      • area()(base * height) / 2
  3. ShapeManager クラス(シングルトン)を作る
    • 以下のメンバを持つ
      • std::vector<std::shared_ptr<IShape>> shapes;
        • ここで図形を管理する
      • static ShapeManager& getInstance()
        • シングルトン実装:ここで唯一の ShapeManager インスタンスを取得
      • void addShape(const std::shared_ptr<IShape>& shape)
        • 受け取った図形を shapes ベクタに追加
      • void printAllShapes()
        • shapes の各図形に対して
          • draw() を呼んで描画メッセージを出力
          • area() を呼んで面積を出力
  4. main() 関数で下記を実装せよ
    • Circle, Rectangle, Triangle をいくつか作成し、ShapeManager::getInstance().addShape(...) で登録
    • ShapeManager::getInstance().printAllShapes() を呼んで、登録された図形をすべて出力

要点

  1. インターフェース(抽象クラス)
    • class IShape { public: virtual void draw() = 0; virtual double area() = 0; virtual ~IShape() = default; };
    • ポリモーフィズムを活用して、draw()area() を統一的に扱う
  2. 継承先クラスの実装
    • Circle, Rectangle, Triangle がそれぞれコンストラクタで必要な値(半径・幅高・底辺高)を保持
    • draw()area()override キーワード付きで実装
    • コンストラクタ・デストラクタでログを出すと挙動がわかりやすいかも(任意)
  3. シングルトンの ShapeManager
    • static ShapeManager& getInstance() { static ShapeManager instance; return instance; }
    • addShape()printAllShapes() を備える
    • printAllShapes() 内でループを回して各図形の draw()area() を呼ぶ
  4. main() でのテスト
    • std::make_shared<Circle>(5) などで図形を生成
    • ShapeManager::getInstance().addShape(...) で登録
    • ShapeManager::getInstance().printAllShapes() で結果を出力

追加アイデア(発展的な要素)

  • ShapeManager のデストラクタにログを入れてみる → 「シングルトンはいつ解放されるか?」がわかる
  • 面積計算時に M_PI<cmath> で定義)を使う
  • usingtypedef を活用してコードをシンプルに
  • エラーチェック(Circle の半径が負の場合はどうするか?など)
  • Triangle 以外にも SquarePolygon を追加してみる

ポイント

  • インターフェース(純粋仮想クラス)で共通機能を定義し、ポリモーフィズムで扱う」
  • シングルトン (ShapeManager) でオブジェクト管理」
  • std::vector<std::shared_ptr<IShape>> を使ってメモリ管理を自動化」

これで前回の学び(インターフェース、ポリモーフィズム、シングルトン、メモリ管理など)を総合的に活かせると思うよ!
ぜひトライしてみてね。うまくいかなかったら「エラーがこう出た!」って質問してくれれば一緒に解決しよう!

最終解答

#include <iostream>
#include <vector>
#include <memory>
#include <cmath>

class IShape{
public:
  virtual void draw() = 0;
  virtual double area() = 0; //constだめ?
  virtual ~IShape() = default;
};

class Circle : public IShape{
public:
    double radius;
    Circle(double r) : radius(r) { std::cout << "Circleコンストラクタ" << std::endl; }
    //~Circle() { std::cout << "Circleデストラクタ" << std::endl; }
    
    void draw() override { std::cout << "Drawing Circle (半径は...)" << std::endl; }
    double area() override {
        return M_PI * radius * radius;
    }
};

class Rectangle : public IShape{
public:
    double width;
    double height;
    Rectangle(double w, double h) : width(w), height(h) { std::cout << "Rectangleコンストラクタ" << std::endl; }
    //~Rectangle() { std::cout << "Rectangleデストラクタ" << std::endl; }
    
    void draw() override { std::cout << "Drawing Rectangle (幅x高さ...)" << std::endl; }
    double area() override {
        return width * height;
    }
};

class Triangle : public IShape{
public:
    double base;
    double height;
    Triangle(double b, double h) : base(b), height(h) { std::cout << "Triangleコンストラクタ" << std::endl; }
    //~Triangle() { std::cout << "Triangleデストラクタ" << std::endl; }
    
    void draw() override { std::cout << "Drawing Triangle (底辺x高さ...)" << std::endl; }
    double area() override{
        return base * height / 2;
    }
};

class ShapeManager{
public:
    static ShapeManager& getInstance(){
        static ShapeManager instance;
        return instance;
    }
    
    void addShape(const std::shared_ptr<IShape>& shape){
        shapes.push_back(shape);
    }
    
    void printAllShapes(){
        for(const auto& shape : shapes){
            shape->draw();
            std::cout << shape->area() << std::endl;
            std::cout << shape.get() << std::endl;
        }
    }

private:
    ShapeManager() {}
    std::vector<std::shared_ptr<IShape>> shapes;

};

int main()
{
    std::shared_ptr<IShape> s1 = std::make_shared<Circle>(5);
    std::shared_ptr<IShape> s2 = std::make_shared<Rectangle>(-3,9);
    std::shared_ptr<IShape> s3 = std::make_shared<Triangle>(4,6);
    
    ShapeManager::getInstance().addShape(s1);
    ShapeManager::getInstance().addShape(s2);
    ShapeManager::getInstance().addShape(s3);
    
    ShapeManager::getInstance().printAllShapes();
    
    std::cout << "&s1:" << &s1 << ",&s2:" << &s2 << ",&s3:" << &s3 << std::endl;

    return 0;
}

最終解答(2周目)

#include <iostream>
#include <cmath>
#include <vector>
#include <memory>

class IShape{
public:
    virtual void draw() = 0;
    virtual double area() = 0;
    
    virtual ~IShape() = default;
};

class Circle : public IShape{
public:
    Circle(double r) : radius(r) {}
    double radius;
    
    void draw() override{
        std::cout << "Drawing Circle(r)..." << radius << std::endl;
    }
    
    double area() override{
        return radius * radius * M_PI;
    }
};

class Rectangle : public IShape{
public:
    Rectangle(double w, double h) : width(w), height(h) {}
    double width;
    double height;
    
    void draw() override{
        std::cout << "Drawing Rectangle (w x h)..." << width << " x " << height << std::endl;
    }
    
    double area() override{
        return width * height;
    }
};

class Triangle : public IShape{
public:
    Triangle(double b, double h) : base(b), height(h) {}
    double base;
    double height;
    
    void draw() override{
        std::cout << "Drawing Triangle (b x h)..." << base << " x " << height << std::endl;
    }
    
    double area() override{
        return base * height / 2;
    }
};

class ShapeManager{
public:
    std::vector<std::shared_ptr<IShape>> shapes; //ptrのvectorじゃないとダメなの?
    
    static ShapeManager& getInstance(){ //これはコンストラクタの一種ではない?
        static ShapeManager instance;
        return instance;
    }
    
    void addShape(const std::shared_ptr<IShape>& shape){
        shapes.push_back(shape);
    }
    
    void printAllShapes(){
        for(auto shape : shapes){
            shape->draw();
            std::cout << "area: " << shape->area() << std::endl;
        }
    }
};

int main()
{
    std::shared_ptr<IShape> s1 = std::make_shared<Circle>(5);
    std::shared_ptr<IShape> s2 = std::make_shared<Rectangle>(10,5);
    std::shared_ptr<IShape> s3 = std::make_shared<Triangle>(3,8);
    
    ShapeManager::getInstance().addShape(s1);
    ShapeManager::getInstance().addShape(s2);
    ShapeManager::getInstance().addShape(s3);
    
    ShapeManager::getInstance().printAllShapes();


/* これではダメ? →ポリモーフィズムを使えない
    Circle c1(10);
    Rectangle r1(20,10);
    Triangle t1(10,5);
*/

    return 0;
}

ふりかえり

  • 関数 override { ...(処理)... } の形で書く
    • 関数 override = (処理); ではない
  • シングルトンの特徴
    • コンストラクタをprivateにする
    • staticで一つだけのインスタンスを作る
  • std::cout << (void型関数) << std::endl; はエラーになる。void型のなかで出力させる
  • 行64:shapes->draw() はミス
    • shapesstd::vector<...>, shapestd::shared_ptr<IShape>
    • いま扱っている変数が一要素なのか、ベクターなのか
  • ポインタの整理
    • shape.get() …それぞれのIshapeオブジェクトのアドレス
    • &shape …std::shared_ptr<IShape> (スマートポインタ)自体のアドレス
  • スマートポインタについて
    • std::make_shared<>newを使わず、shared_ptr<> を作れる
    • スマートポインタから値を取り出したいとき、(*shape).area() の形もとれるが非推奨
  • C++のコードを読み始め、学習を始めて3日目くらいでこの演習はキツい

ふりかえり (2周目、4/6)

  • 抽象クラスIShapeはインスタンスを作れないから、std::vector<IShape>も無理
  • class Circle : public IShape{…}publicの意味:継承のアクセス指定子
    • public: 親クラスのpublicメンバを、子クラスでもpublicとして扱える
    • private: 親クラスのpublicメンバが、子クラスではprivateになる
  • ポリモーフィズム(多態性)を使うには、ポインタor参照必須
    • ポリモーフィズム:同じ型で異なる振る舞いを実行できる仕組み
    • スライシング:子クラスの情報が、親クラスの変数にコピーされたときに消える現象
IShape s = Circle(5); // エラー、オブジェクトのスライシング
IShape* s = new Circle(5); // OK
  • 型& 変数名 = 値の場合、参照する(エイリアスを作っている)
    • &は参照型の意味
    • そのため、int& x = 4; など右辺が定数の場合はエラーになる。
    • ただし、const int& x = 4; はOK(テンポラリ参照というらしい)
Circle c;
IShape& shape = c; //shape, c はどちらも同じオブジェクトを見ている
  • カプセル化:オブジェクトの内部データや実装を隠して、安全に使わせる設計
    • アクセスの窓口(getter/setter,操作メソッド)だけをpublicにする
    • クラスの外から呼び出す操作(API)→public
    • 内部のデータ構造やロジック→private
    • 外部にも渡して良い読み取り専用の情報→public
    • API: Application Programming Interface プログラム同士のやりとりを決める入口
    • 自分でクラスを作ったら、そのpublicな部分がAPIになるイメージ
  • static のイメージ:インスタンスに紐付かず、プログラム全体で一つだけ存在するもの
    • static変数(関数内)、staticメンバ関数、staticメンバ変数
    • staticメンバ関数の使い道:
      • シングルトン インスタンス1つだけほしい
      • ユーティリティ関数 日付変換など状態がいらない処理を集約できる
      • ファクトリメソッド 初期状態が決まったインスタンスを返す
    • クラスはオブジェクトの表現だけでなくて、カテゴリ化して機能をグルーピングするためにも使える
  • シングルトンと同じ実装を、他の書き方で実現できないのか?
    • できなくはないがベストではない。
    • シングルトンの目的:①プログラム内でインスタンスがただ1つ、②どこからもアクセス可能にしたい。
    • 代案1 グローバル変数で持つ:どこからも書き換え可能、テストしにくい、初期化順の保証がないデメリット。C++では、グローバル変数の初期化順はファイルを跨ぐと未定義となる。初期化の順番によってはクラッシュの原因になる。
    • 代案2 static変数を名前空間で囲む(他のファイルから見えなくする):クラスにカプセル化されない、自然な書き方にならない、保守性低下につながるデメリット。

第3問 ディスパッチャー (3/2)

問題:「ディスパッチャーにアクションを登録して実行する流れ」を小さく再現

じゃあ今日の復習を兼ねて、簡易版の「Dispatcher + ActionController」システムを作るコーディング問題 を出すね。今回学んだ「ディスパッチャーにアクション(=コールバック)を登録して実行する流れ」を小さく再現してみよう!


仕様

  1. ActionCode という enum を作り、いくつかのアクションを定義する
  2. Dispatcher クラス を作り、アクションコードハンドラー(関数ポインタ or ラムダ) を保存しておく
  3. registerAction() メソッドを持つ ActionController を作り、そこから Dispatcher にアクションを登録する
  4. isEnabled(アクションが有効かどうかを返す関数)も登録し、アクション実行前にチェック してみて
  5. メイン関数 から「アクション登録 → アクション実行」の流れを実装し、有効・無効で動きが変わる ことを確認する

要件詳細

  1. enum class ActionCode
    • 例:PrintHello, PrintWorld, Exit など、何かしら3つくらい定義
  2. Dispatcher クラス
    • メンバ
      • std::unordered_map<ActionCode, std::function<void()>> handlers;(アクションと処理の紐づけ)
      • std::unordered_map<ActionCode, std::function<bool()>> enablers;(アクションが有効かどうかを判定する関数)
    • メソッド
      • void reg(ActionCode, std::function<void()>, std::function<bool()>)(アクションを登録)
      • void dispatch(ActionCode)(登録されたアクションを呼ぶ。呼ぶ前に「enabler」が true かチェック)
  3. ActionController クラス
    • メンバ
      • Dispatcher* dispatcher;(外部から渡してもいいし、内部で保持してもOK)
    • メソッド
      • registerAction(ActionCode code, std::function<void()> handler, std::function<bool()> enabler)
        • → 中で dispatcher->reg(code, handler, enabler); を呼ぶだけでもOK
      • アクションを呼び出すメソッド(なくてもメインで dispatcher->dispatch(...) してもいい)
  4. メイン関数
    • 流れ(例)
      1. ActionCode のいくつかの値に対し、ハンドラー有効/無効判定関数 を登録
      2. ユーザーにアクションコードを入力させる or 順番に dispatcher->dispatch(...) を呼ぶ
      3. アクション実行前に enabler が false の場合は「無効なアクションです」と表示
      4. true ならハンドラーを実行 → 出力が確認できればOK

ヒント

  • ラムダ式 でハンドラーとエネーブラ(isEnabled)を作るとスッキリ
  • たとえば
    std::function<void()> handler = []() { std::cout << "Hello!\n"; };
    std::function<bool()> enabler = []() { return true; };
  • std::bind を使ってメンバ関数ポインタを紐づけるのもアリ
  • アクションコードハンドラー / エネーブラ を登録し、
    dispatch で「どのアクションを呼びたいか?」を指定 → 実際に関数が動く流れを作ればOK

提出イメージ

  • ヘッダファイルでも .cpp でもどっちでもOK
  • 最低限ビルド可能な形にして、メインでアクションを登録 → dispatch → 出力テストを見せてくれたら嬉しい!

今回のポイントは 「Dispatcher を使って、アクションを登録し、アクションコードを指定して実行する流れを作る」 こと。
実際の notationactioncontroller.cpp に近い設計を めちゃ小さく再現 するイメージだよ。

もし時間があれば、「メンバ変数を切り替えるアクション」 を入れたり、「enabler が false になる条件」 を作ったりすると、より理解が深まるよ!

がんばれー!

最終解答

#include <iostream>
#include <unordered_map>
#include <functional>

enum class ActionCode {
    PrintMem,
    PrintRuby,
    PrintKana
};

class Dispatcher {
private:
    std::unordered_map<ActionCode, std::function<void()>> handlers;
    std::unordered_map<ActionCode, std::function<bool()>> enablers;

public:
    Dispatcher(){

        // ハンドラー
        handlers[ActionCode::PrintMem] = []() {
            std::cout << "やあ!MEMちょだよ!" << std::endl;
        };
        
        handlers[ActionCode::PrintRuby] = []() {
            std::cout << "みんなおまたせ! ルビーだよ" << std::endl;
        };
        
        handlers[ActionCode::PrintKana] = []() {
            std::cout << "待たせたわね。かなよ" << std::endl;
        };
        
        // エネーブラー
        enablers[ActionCode::PrintMem] = []() { return true; };
        enablers[ActionCode::PrintRuby] = []() { return true; };
        enablers[ActionCode::PrintKana] = []() { return false; };
    }
    
    // register
    void reg(ActionCode code, std::function<void()> handler, std::function<bool()> enabler) {
        handlers[code] = handler;
        enablers[code] = enabler;
    }
    
    // Dispatch
    void dispatch(ActionCode code){
        if(enablers[code] && !enablers[code]()){
            std::cout << "無効" << static_cast<int>(code) << std::endl;
            return;
        }
        
        if(handlers.find(code) != handlers.end()){
            handlers[code]();
        } else {
            std::cout << "未登録" << std::endl;
        }
    }
    
};

class ActionController{
    Dispatcher* dispatcher;
    
    void registerAction(ActionCode code, std::function<void()> handler, std::function<bool()> enabler){
        dispatcher->reg(code, handler, enabler);
    }
};

int main()
{
    Dispatcher dispatcher;
    
    dispatcher.dispatch(ActionCode::PrintMem);
    dispatcher.dispatch(ActionCode::PrintRuby);
    dispatcher.dispatch(ActionCode::PrintKana);
    
    return 0;
}

最終解答(2周目)

#include <iostream>
#include <unordered_map>
#include <functional>

enum class ActionCode{
  midiPlay,
  audioPlay,
  putNote
};

class Dispatcher{
public:
    std::unordered_map<ActionCode, std::function<void()>> handlers;
    std::unordered_map<ActionCode, std::function<bool()>> enablers;
    
    //複数の処理・実行可否をひとつのdispatcherにまとめる
    void reg(ActionCode code, std::function<void()> handler, std::function<bool()> enabler){
        handlers[code] = handler;
        enablers[code] = enabler;
    }
    
    //ハンドラーの実行
    void dispatch(ActionCode code){
        if (handlers.find(code) == handlers.end()){
            std::cout << "Action未登録: " << static_cast<int>(code) << std::endl;
            return;
        }
        
        if (enablers.find(code) != enablers.end() && !enablers[code]()){
            std::cout << "Actionは現在無効です: " << static_cast<int>(code) << std::endl;
            return;
        }
        
        handlers[code]();
    }
};

class ActionController{
public:
    Dispatcher* dispatcher;
    ActionController(Dispatcher* disp) : dispatcher(disp){}
    
    void registerAction(ActionCode code, std::function<void()> handler, std::function<bool()> enabler){
        dispatcher->reg(code, handler, enabler);
    }
    
    void registerAll(){
        registerAction(ActionCode::midiPlay,
            [](){ std::cout << "MIDI再生" << std::endl; },
            [](){ return true; }
        );
        
        registerAction(ActionCode::audioPlay,
            [](){ std::cout << "オーディオ再生" << std::endl; },
            [](){ return false; }
        );
        
        registerAction(ActionCode::putNote,
            [](){ std::cout << "音符を入力" << std::endl; },
            [](){ return true; }
        );
    }
};

int main()
{
    Dispatcher dispatcher;
    ActionController controller(&dispatcher);
    
    controller.registerAll();
    
    dispatcher.dispatch(ActionCode::midiPlay);
    dispatcher.dispatch(ActionCode::audioPlay);
    dispatcher.dispatch(ActionCode::putNote);

    return 0;
}

ふりかえり

  • エラーを出した原因
    • enum class は列挙型→メンバ関数を持たせることはできない
      • メンバ関数を持たせるなら、通常のクラスか構造体にする
    • クラスのメンバ変数の初期化をクラス内部で直接行わない→コンストラクタ内で初期化
    • 他にもたくさんミスを出しているが、根本的に概念の整理ができていないからだと思う
  • enablerでは、アクション実行の条件を決めておき、現在の状態に応じてtrue/falseを返す
    • 条件の例:「選択中の要素がある」「演奏中でない」「テキスト編集中でない」
  • 行46:enablers[code] && !enablers[code]()
    • enablers[code]が存在する(nullptrでない) かつ enablers[code] の戻り値がfalse
  • いまの実装だと、Actioncontrollerはいらないが、将来的な拡張を考えると必要らしい
    • よくわからん
    • Actioncodeの実装は上の例だとふさわしくないかも。「演奏中」などにしたほうがよい?
  • find(), end()
    • auto it = container.find(key):keyに対応する要素を探してそのイテレータを返す
    • キーが存在しなければ、end()を返す
  • STLコンテナ
    • C++のSTL(Standard Template Library)に含まれるデータ構造
    • 順序コンテナ:順番を保持する vector, list, deque, array
    • 連想コンテナ:キーと値をペアで管理する map, unordered_map, set, unordered_set
    • アダプタコンテナ:他のコンテナを内部で使い、特定の機能を提供 stack, queue, priority_queue
  • std::function
    • 関数ポインタをラップするクラス。関数を変数のように扱ったり、引数として渡せる
    • std::function<void()> func = [](){ ... };:ラムダ式を格納できる
  • std::bind
    • 関数の一部の引数を固定して、新しい関数オブジェクトを作る
    • std::bind(関数ポインタ, 引数1, 引数2, …, std::placeholders::_1, …)

ふりかえり (2周目、4/5)

  • ディスパッチャー:複数の処理と実行可否をまとめて管理する
    • メンバ関数の役割
    • reg:ActionCode、処理内容、判定可否の組み合わせを登録
    • dispatch:入力されたActionCodeの実行
  • unordered_map で、存在しないキーを参照すると新たに作られる
    • handlers[code] で、もしcodeがなければ新たに要素が作られる
    • →存在チェック (falseが返ってくることを期待)には使えない
handlers.find(code) == handlers.end(); // → 存在チェック
handlers[code]; // → 存在しなければ 新しい要素が作られる!
  • dispatchの実装
    • if elseで実行ダメな場合と実行可能な場合を振り分け続ける
      → 関数全体をifで覆わないといけない、ネストが深くなる、条件漏れが怖い
    • はじめに実行ダメな場合を if で拾って早期 return、最後に本処理をネストなしで書くとわかりやすい
  • コンストラクタの復習
    • インスタンスが作られるときに実行されるメソッド。変数の初期化にも使える。
    • ActionController(Dispatcher* disp) : dispatcher(disp) {}
      このクラスが持つメンバ変数dispatcherに、コンストラクタ引数のdispを代入するという初期化をしている。
    • この定義を外すと、インスタンス生成で引数として渡しても、クラス変数としてその引数を「持った」とはいえないので、ポインタなら未定義動作につながる。
  • Dispatcher* dispatcher;の必要性
    この行がないと、ActionControllerクラスはメンバ変数としてdispatcherを持たなくなるため、後の処理で毎回dispatcherのポインタを引数として渡さないといけなくなる

第4問 enum(列挙型)、リスト、条件分岐 (3/6)

問題:音符・休符リストの選択範囲を拡張するプログラム

あなたは楽譜編集ソフトを作っています。
ChordRest(音符・休符)のリストが与えられたとき、現在選択中の ChordRest を基準に 選択範囲を拡張 する処理を作成してください。


条件

  1. 音符をリスト(std::vector<int>)として管理し、それぞれの音を数値で表す
    • 例: { 60, 62, 64, 65, 67 }C, D, E, F, G
  2. 現在選択されている音符のインデックスと、拡張方向(ExpandSelectionMode)を指定
  3. 拡張した結果、選択範囲の最小値と最大値をコンソール出力する
  4. モードによって選択範囲を以下のように拡張
    • BEGIN → リストの最初の音を選択
    • END → リストの最後の音を選択
    • NEXT → 次の音に拡張(最後の音なら変化なし)
    • PREV → 前の音に拡張(最初の音なら変化なし)

関数のシグネチャ

enum class ExpandSelectionMode {
BEGIN,
END,
NEXT,
PREV
};

void expandSelection(const std::vector<int>& notes, int& start, int& end, ExpandSelectionMode mode);

入出力の例 (一部省略)

入力

std::vector<int> notes = { 60, 62, 64, 65, 67 };  // C, D, E, F, G
int start = 1, end = 2; // 最初に選択されている範囲 (D, E)

expandSelection(notes, start, end, ExpandSelectionMode::BEGIN);

出力

選択範囲: C - E

挑戦ポイント

  • 配列の境界(最小・最大)を超えないように処理する
  • ExpandSelectionMode に応じて startend を適切に変更する
  • 変更後の notes[start] から notes[end] の範囲を出力する

今日学んだ「選択範囲の拡張」をコンソール出力で試してみよう!
コードが書けたらここに投稿してくれればレビューするよ!

最終解答

#include <iostream>
#include <vector>
#include <unordered_map>

enum class ExpandSelectionMode {
	BEGIN,
	END,
	NEXT,
	PREV
};

std::string getNoteName(int note){
    const std::unordered_map<int, std::string> noteMap = {
        {60, "C"}, {61, "C#"}, {62, "D"}, {63, "D#"}, {64, "E"},
        {65, "F"}, {66, "F#"}, {67, "G"}, {68, "G#"}, {69, "A"},
        {70, "A#"}, {71, "B"}
    };
    
    if(noteMap.find(note) != noteMap.end()){
        return noteMap.at(note);
    }
    return "Unknown";
};

void expandSelection(const std::vector<int>& notes, int& start, int& end, ExpandSelectionMode mode) {
    // modeに応じてstart-endの範囲を変更
    if (mode == ExpandSelectionMode::BEGIN){
        start = 0;
    } else if (mode == ExpandSelectionMode::END){
        end = notes.size() - 1;
    } else if (mode == ExpandSelectionMode::NEXT){
        if (end != notes.size() - 1){
            end++;
        }
    } else {  //PREV
        if (start != 0){
            start--;
        }
    }
    
    //start,endの音名を表示
    std::cout << "start:" << getNoteName(notes[start]) << std::endl;
    std::cout << "end:" << getNoteName(notes[end]) << std::endl;
}

int main()
{
	std::vector<int> notes={60, 62, 64, 65, 67, 71, 63};
	int start = 3, end = 5;

    expandSelection(notes, start, end, ExpandSelectionMode::END);
	return 0;
}

最終解答(2周目)

#include <iostream>
#include <vector>
#include <unordered_map>

enum class ExpandSelectionMode{
    BEGIN,
    END,
    NEXT,
    PREV
};

std::string pitchToNote(int pitch){
    static const std::vector<std::string> names = {"C", "C#", "D", "D#", "E", "F",
                                                 "F#", "G", "G#", "A", "A#", "B"};
    int octave = (pitch / 12) - 1;
    
    return names[pitch % 12] + std::to_string(octave);
}

void ExpandSelection(const std::vector<int>& notes, int& start, int& end, ExpandSelectionMode mode){
    switch(mode){
        case ExpandSelectionMode::BEGIN:
            start = 0;
            break;
        case ExpandSelectionMode::END:
            end = notes.size() - 1;
            break;
        case ExpandSelectionMode::NEXT:
            end += (end == notes.size() - 1) ? 0 : 1;
            break;
        case ExpandSelectionMode::PREV:
            start -= (start == 0) ? 0 : 1;
            break;
    }
}

int main()
{
    std::vector<int> notes = {60, 62, 66, 69, 73};
    int start = 1;
    int end = 3;
    
    ExpandSelection(notes, start, end, ExpandSelectionMode::NEXT);
    
    std::cout << "範囲変更:" << pitchToNote(notes[start]) << " - " <<
        pitchToNote(notes[end]) << std::endl;
    
    return 0;
}

ふりかえり

  • 関数シグネチャ:関数の名前、引数、戻り値の型を定義したもの
  • 関数を宣言する順番を間違えると、スコープの問題が発生することがある
    • プロトタイプ宣言でも対処可能
  • C++ では関数のネスト(関数の中で関数定義)はサポートされない
    • どうしても必要ならラムダ式

ふりかえり (2周目、4/5)

  • 音名と数値の対応づけ:前回より効率的にできた。
  • std::string names[] よりは、std::vector<std::string> がいまふう
  • 文字列型にキャストstd::to_string(hoge)
  • 文字列のドッギングは a+b でOK → “ab”と表示される
  • namesstatic const がいいかも
  • 今後の拡張ポイント:♯←→♭の切り替えロジック(調性、前後の文脈から判断させる)

第5問 ポインタ、参照 (3/7)

問題:動的配列を拡張する関数を実装

C++ で int**(ポインタのポインタ)と int&(参照)」 を使って、コンソール出力だけで完結するプログラムを作りましょう!


問題の内容

  1. expandArray(int** arr, int& size) を作成せよ。
    • 引数:
      • int** arr : 動的に確保した int 配列へのポインタ(ポインタのポインタ)。
      • int& size : 現在の配列サイズ(参照渡し)。
    • 処理:
      1. 新たに size + 2 の動的配列を確保 する。
      2. 古い配列の要素をコピー する。
      3. 追加された 2 つの要素に任意の値(例: 100, 200)を代入 する。
      4. 古い配列を解放し、新しい配列を *arr に設定 する。
      5. sizesize + 2 に更新 する。
      6. 拡張後の配列をコンソールに出力 する。
  2. main() での流れ
    • int* data を動的に確保し、3個の要素 を用意して好きな値を入れる(例: 10, 20, 30)。
    • 変数 int size = 3; として、配列サイズを管理。
    • expandArray(&data, size) を呼び出して配列を拡張!
    • 拡張後の配列を再度コンソール出力 して、拡張結果を確認。

ヒント

  1. ポインタのポインタ (int** arr) を使うことで、関数内で arr 自体を新しいメモリに貼り直せる
  2. 配列サイズは int& size として参照渡しすることで、関数内で更新した値を呼び出し元に反映できる
  3. 動的配列は newdelete[] を使って確保・解放する。

チャレンジのポイント

  • ポインタのポインタ を使って、関数内で配列を再割り当て できるようにする
  • 参照 (int&) で配列サイズを更新し、呼び出し元にも反映
  • ちゃんと 古いメモリを解放 (delete[]) して、新しいメモリを割り当て する
  • コンソール出力は expandArray() 内と main() 両方 で確認

ポインタ、参照、動的メモリ確保を上手く使って、拡張後の配列を正しく表示できるプログラムを書いてみよう!
解けたらぜひソースコードを貼ってみてください。レビューしますよ!頑張ってね!

最終解答

#include <iostream>

void expandArray(int** arr, int& size){
    int* expandarr = new int[size + 2];
    std::copy(*arr, *arr+size, expandarr);
    
    expandarr[size] = 100;
    expandarr[size+1] = 200;
    
    delete[] *arr;
    *arr = expandarr;
    size += 2;
    
    std::cout << "new arr:";
    for(int i=0; i < size; i++){
        std::cout << (*arr)[i] << " ";
    }
    std::cout << std::endl;
    
    std::cout << "expandarr(int*):" << expandarr << std::endl;
    std::cout << "arr(int**):" << arr << std::endl;
    std::cout << "*arr(int*):" << *arr << std::endl;
    std::cout << "*arr+size(int*):" << *arr+size << std::endl;
}

int main()
{
    int* data = new int[3]{10,20,30};
    int size = 3;
    
    std::cout << "data:";
    
    for(int i=0; i < size; i++){
        std::cout << data[i] << " ";
    }
    
    std::cout << std::endl;
    expandArray(&data, size);
    
    std::cout << "(new)data:";
    for(int i=0; i < size; i++){
        std::cout << data[i] << " ";
    }
    
    std::cout << std::endl;
    return 0;
}

最終解答(2周目)

#include <iostream>

void expandArray(int** arr, int& size){
    int* newArr = new int[size+2]{};
    newArr[0] = (*arr)[0];
    newArr[1] = (*arr)[1];
    newArr[2] = (*arr)[2];
    
    newArr[3] = 100;
    newArr[4] = 200;
    
    delete[] *arr;
    *arr = newArr;
    
    size += 2;

    std::cout << "updated *arr: ";
    for(int i=0; i<size; ++i){
        std::cout << (*arr)[i] << " ";
    }
    std::cout << std::endl;
    
    //delete[] newArr; 二重解放エラーになる
}


int main()
{
    int* data = new int[]{3,4,5};
    int size = 3;
    
    expandArray(&data, size);
    
    std::cout << "updated data: ";
    for(int i=0; i<size; ++i){
        std::cout << data[i] << " ";
    }
    std::cout << std::endl;

    delete[] data;
    return 0;
}

ふりかえり

  • 行4:int* expandarr = new int[size + 2];
    • この形で配列を宣言すると、expandarrはポインタとなる(newはポインタを返す)
  • delete[] *arr; すべき場所に注意
    • 正しくdeleteしないとメモリリークが発生するが、タイミングを間違えると新しいデータを削除してしまう(わかってない)
    • C++では変数のメモリ確保の方法に2種類ある。
    • スタック領域:関数のスコープ内で宣言された変数が確保される場所。自動的に開放
    • ヒープ領域:プログラム実行中に自由にメモリ確保・開放される。new,deleteが必要
  • int** だけでなく int*& (ポインタ参照)も構文的には可能
  • std::copybegin(), end()
    • int*begin(),end()は使えない。std::vector<int>なら可能
    • copyの引数にはアドレスを渡す
  • int*型のdataに対して、(*data)[i]と参照するのは誤り
    • *dataはint型と扱われるので、int[i]という無効な参照になる。data[i]でよい
    • または、*(data+i)
  • arr[0]=10, arr[1]=20, ptr=&arrのとき、
    • **ptr=arr[0]=10, **ptr+1=arr[0]+1=11, *(*ptr+1)=arr[1]=20

ふりかえり (2周目、4/3)

  • 二重ポインタint** arr, *arr = data = new int[]{3,4,5}に対して、
    (*arr)[0]*arr[0]は意味が違う。
    • *arr[1] = *(arr+1)なので、arrという一つしかないint*型の値の次を要求している
    • →存在しないポインタを参照してしまう
    • cf. (*arr)[1] = data[1] = 4
  • ポインタの二重解放エラー
    • newして呼び出し元に渡したら解放しなくてよい
  • delete[] *arrをすると、*arr = dataが指す配列{3,4,5}の領域が開放される。dataのアドレスが消去されたり、配列の中身は書き換えられない。

第6問 ベクター、条件分岐、ループの処理 (3/10)

問題:音符リストのナビゲーションを実装

以下の要件を満たす C++ コンソールプログラムを作成してください。

  1. 音符 (Note) を表す構造体またはクラス Note を用意する。
    • メンバとして
      • int pitch (ピッチを表す、例: 60 = 中央ド付近)
      • bool isRest (休符かどうか)
    • 休符の場合、pitch の値は無視してよい。
  2. 音符のリスト (std::vector<Note>) を用意し、コンソール入力から値を受け取り初期化する。
    • まず N (音符の数) を読み込む。
    • その後、N 個の行について
      • pitchisRest (0/1 や true/false などの形式は自由) を入力する。
  3. 移動処理 (ナビゲーション) を実装 する。
    • コンソールから以下のコマンドを繰り返し受け付ける:
      • move next → 現在位置から「次の休符でない音符」へ移動
      • move prev → 現在位置から「前の休符でない音符」へ移動
      • quit → 終了
    • 移動先が存在しない場合は、「移動できません」 とメッセージを表示して、現在位置は変えない。
  4. 現在位置 (index) を管理し、move next / move prev コマンドのたびに更新する。
    • プログラム開始時は 0番目 (先頭要素) を現在位置 とする。
    • move next:
      • 現在位置より後ろの要素で isRest == false な音符があれば、そこへ移動
      • 見つからなければエラーメッセージを表示し、位置は変えない
    • move prev:
      • 現在位置より前の要素で isRest == false な音符があれば、そこへ移動
      • 見つからなければエラーメッセージを表示し、位置は変えない
  5. 各コマンド実行後、現在位置をコンソールに出力 する。
    • 例えば「現在位置: 3, pitch=67」といった形でOK (フォーマットは自由)。
  6. コマンド quit が入力されたら、プログラムを終了 する。

入出力イメージ (略)

上記はあくまで一例です。入出力の形式は多少変えても構いませんが、

  • 音符の情報を N 個読み込む
  • move next / move prev / quit を繰り返し入力できる
  • そのたびに結果をコンソールに表示する

という部分を満たしてください。

ポイント

  • ベクター (std::vector<Note>) の使い方
  • 休符 (isRest == true) をスキップ して移動するロジック
  • 現在位置を越えて移動しないように条件分岐
  • 失敗時のメッセージ出力

この問題を解くことで、

  • C++ のベクター操作
  • ループを使った入力処理
  • 条件分岐を使った探索 (次 or 前) の実装
  • コンソール出力

などを練習できるはずです。頑張ってください!

最終解答

#include <iostream>
#include <vector>
#include <string>

class Note{
public:
    int pitch;
    bool isRest;
};

int main()
{
    std::cout << "音符の数(N):...";
    
    int n;
    std::cin >> n ;
    
    std::vector<Note> newNote;
    
    int p;
    bool r;
    
    //音符リスト入力
    for(int i=0; i < n; i++){
        std::cout << "i=" << i << " , pitch, rest(0/1)...";
        std::cin >> p >> r ;
        newNote.push_back({p,r});
    }
    
    //移動処理
    int idx = 0;
    std::string str= "";
    
    std::cout << "入力完了。移動:n/p/q...";
    std::cin >> str;
    
    while(true){
        bool moved = false;
        
        if(str=="n"){
            for(int i = idx+1; i < n; i++){
                if(!newNote[i].isRest){
                    idx = i;
                    moved = true;
                    std::cout << "移動:idx=" << idx << " ";
                    break;
                }
            }
        }else if(str=="p"){
            for(int i = idx-1; i > -1; i--){
                if(!newNote[i].isRest){
                    idx = i;
                    moved = true;
                    std::cout << "移動:idx=" << idx << " ";
                    break;
                }
            }
        }else if(str=="q"){
            break;
        }
        
        if(!moved){
            std::cout << "移動できません" << std::endl;
        }
        
        std::cout << "n/p/q...";
        std::cin >> str;
    }
    
    std::cout << "終了" << std::endl;
    std::cout << "現在位置:" << idx << " , pitch:" << newNote[idx].pitch << std::endl;
    
    //for(const Note& note : newNote){
    //    std::cout << "Pitch:" << note.pitch << ",Rest:" << (note.isRest ? "Yes" : "No") << std::endl;
    //}

    return 0;
}

最終解答(2周目)

#include <iostream>
#include <vector>

struct Note{
    int pitch; //休符=0
    bool isRest;
};

int main()
{
    int N;
    std::cout << "N: ";
    std::cin >> N;
    
    std::vector<Note> note;
    int p;
    bool r;
    
    for(int i=0; i<N; ++i){
        std::cout << "i=" << i << ": ";
        std::cin >> p >> r;
        note.push_back({p,r});
    }
    
    //ナビゲーション実装
    std::string navi;
    int idx = 0;
    bool canmove = false;
    
    while(true){
        std::cout << "コマンド受付 n/p/q: ";
        std::cin >> navi;
        
        if(navi=="n"){
            canmove = false;
            
            if(idx < N-1){ //idx=後ろから2番目のときまで
                for(int i=idx+1; i<N; ++i){
                    if(!note[i].isRest){
                        std::cout << "i=" << i << ", pitch=" << note[i].pitch << std::endl;
                        canmove = true;
                        idx = i;
                        break;
                    }
                }
            }
        }else if(navi=="p"){
            canmove = false;
            
            if(0 < idx){
                for(int i=idx-1; -1<i; --i){
                    if(!note[i].isRest){
                        std::cout << "i=" << i << ", pitch=" << note[i].pitch << std::endl;
                        canmove = true;
                        idx = i;
                        break;
                    }
                }
            }
            
        }else if(navi=="q"){
            std::cout << "終了";
            break;
        }else{
            std::cout << "コマンドが不正です。" << std::endl;
        }
        
        if(!canmove){
            std::cout << "移動できません。" << std::endl;
        }
    }

    return 0;
}

ふりかえり

  • cinの使い方
    • cin >> p,r だとエラー。cin >> p >> r
  • vectorへの代入:push_back()
    • 複数要素をまとめるなら、push_back({p,r})のようにする
  • 行18~28:vector<Note>の宣言で確保した後にさらにpush_backするのは誤り
    • std::vector<Note> newNote(n); // すでにn個確保
    • newNote.push_back({p,r}); //n個枠のあとにさらにデータを追加してしまう
  • 行42など:vector<Note> 自体に、.pitchという形でメンバ変数にアクセスできない
    • newNotestd::vector<Note> ならば、newNote[0].pitch という表記でNoteオブジェクトのメンバ変数にアクセスできる
  • 行37以降のwhile処理
    • 行43,52でidx++; にしていたがバグの原因になった
    • movedフラグを立てておくと、移動できない場合の処理を簡潔にかける
  • 文字列型は、Cスタイルのchar, char配列もあるが、C++ではstd::stringが一般的
  • 出題の要求仕様はよくある内容で理解も容易いのに、正しく動くコードを書くのが意外と難しい
    • どこで条件分岐させるか、while文をどのタイミングでbreakさせるか
    • 変数の型の勘違いも多発。ベクターなのかNoteオブジェクトなのか
    • 高度なテクニックより、こういった処理を地道にミスなくできるのも実力のひとつ。コードをざっくり読んでいるだけだと気づかないなあ
    • 数学の問題で計算ミスをゼロにする努力と似ているかも

ふりかえり (2周目、3/31)

  • フラグの使い方:フラグは1回だけ立てる
    • 立てたり降ろしたりしていると、デバッグ作業中に混乱する。
    • はじめに私が書いていたイケてないコード↓
    • 処理を追うと、canmoveフラグがtrueになったりfalseになったりしていてわかりづらい
  • ネストを深くしない工夫をしたい
    • 例:2重ループの抜け方
    • 内側ループはbreak、そこでフラグを立てる
    • 外側ループはフラグ検知したらbreak。あるいは、関数化して抜けたい場所でreturn
    //ナビゲーション実装
    std::string navi;
    int idx = 0;
    bool canmove = true;  //移動先が存在すると仮定しておく
    
    while(true){
        std::cout << "コマンド受付 n/p/q: ";
        std::cin >> navi;
        
        if(navi=="n"){
            if(idx<N){
                for(int i=idx+1; i<N; ++i){
                    if(note[i].isRest == false){
                            std::cout << "i=" << i << ", pitch=" << note[i].pitch << std::endl;
                            canmove = true; //移動先が存在した! true確定
                            idx = i;
                            break;
                    }
                    canmove = false;  //次の移動先は休符→今回は移動可能じゃない! falseに変更
                }
            }else{
                canmove = false; //現在地がそもそも idx = N のとき(←実は誤り、N-1のときの勘違い)後ろの音符がないから false に変更
            }

第7問 フィルタ、ソート (3/16)

問題:有効/無効を持つ譜面セグメントをフィルタ&ソートして出力

MuseScoreのコードリーディングで学んだ内容を活かしつつ、コンソールへ結果を出力するC++プログラムを作ってください。以下の条件を満たすように実装してください。

前提

  • Segment構造体
    • 以下のメンバを持つ:
      • int tick; : そのセグメントの時間(拍)位置を表す
      • bool enabled; : セグメントが有効か無効かを示す
  • filterAndSortSegments関数
    • std::vector<Segment> を受け取り、
    • 「enabled が true のセグメントだけ」 を取り出し、
    • tick の昇順でソート した結果を返す
  • main関数
    • 適当な std::vector<Segment> を用意し、
    • filterAndSortSegments を使って整列・フィルタした結果を受け取り、
    • 「tick と enabled の値」をコンソールに出力 する

要求仕様

  1. 「enabled が false」のセグメントは無視(フィルタ)
  2. 「enabled が true」のセグメントのみ、tick の小さい順にソート
  3. 結果をコンソールに出力
    • 出力形式は任意ですが、各セグメントの tickenabled を分かりやすく表示してください
  4. main関数で動作が分かるように、複数のセグメントを用意してテスト

最終解答

#include <iostream>
#include <vector>

struct Segment{
    int tick;
    bool enabled;
};

void filterAndSortSegments(std::vector<Segment>& seg){
    std::vector<Segment> newSeg;
    
    for(const auto& Segment : seg){
        if(Segment.enabled == true){
            newSeg.push_back(Segment);
        }
    }
    
    std::sort(newSeg.begin(), newSeg.end(), [](auto const& a, auto const& b){
        return b.tick > a.tick;
    });
    
    seg = newSeg;
};


int main(){
    
    std::vector<Segment> exampleSeg1 = {
        {10,true},
        {6,false},
        {5,true},
        {2,true}
    };
    
    std::vector<Segment> exampleSeg2 = {
        {3, true},
        {9, false},
        {4, true},
        {20, true},
        {14, true}
    };
    
    filterAndSortSegments(exampleSeg1);
    filterAndSortSegments(exampleSeg2);
    
    std::cout << "exampleSeg1 result" << std::endl;
    for(const auto& Segment : exampleSeg1){
        std::cout << "tick: " << Segment.tick << " , enabled: " << Segment.enabled << std::endl;
    }

    std::cout << "exampleSeg2 result" << std::endl;
    for(const auto& Segment : exampleSeg2){
        std::cout << "tick: " << Segment.tick << " , enabled: " << Segment.enabled << std::endl;
    }


    return 0;
}

最終解答(2周目)

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

struct Segment{
    int tick;
    bool enabled;
};

std::vector<Segment> filterAndSortSegments(std::vector<Segment>& seg){
    
    std::vector<Segment> newSeg;
    for(const auto& el: seg){
        if(el.enabled == true){
            newSeg.push_back(el);
        }
    }
    
    std::sort(newSeg.begin(), newSeg.end(), [](Segment a, Segment b){
        return a.tick < b.tick;
    });
    
    return newSeg;
}


int main()
{
    std::vector<Segment> exSeg1 = {
        {10,true},
        {6,false},
        {5,true},
        {2,true}
    };
    
    std::vector<Segment> exSeg2 = {
        {3, true},
        {9, false},
        {4, true},
        {20, true},
        {14, true}
    };
    
    std::vector<Segment> exSeg3 = filterAndSortSegments(exSeg1);
    std::vector<Segment> exSeg4 = filterAndSortSegments(exSeg2);

    std::cout << "exseg1 sorted: " << std::endl;
    for(const auto& el : exSeg3){
        std::cout << el.tick << " " << el.enabled << std::endl;
    }
    
    std::cout << "exseg2 sorted: " << std::endl;
    for(const auto& el : exSeg4){
        std::cout << el.tick << " " << el.enabled << std::endl;
    }
    
    return 0;
}

ふりかえり

  • 行10:const std::vector<Segment>& で参照させよう。コピーしないので速くなる
  • 行11:newSeg は reserveするとメモリ効率が良くなるらしい(わかってない)
    • newSeg.reserve(seg.size());
  • 行19:sort処理の第3引数のかきかた
    • auto const a, auto const b にするとコピーが発生するので参照しよう
    • ラムダ式の引数には、型かオブジェクト全体しか指定できないので、a.tick などは代入不可
  • 行44-45:はじめここを、exampleSeg1 = filterAndSortSegments(exampleSeg1); と書かないと正しく処理されなかった。その原因を追及
    • filterAndSortSegmentsの戻り値の型:void vs. std::vector<Segment>
    • ②関数の中にreturnがあるかないか
    • ③引数はコピーをとるのか、参照渡しなのか
    • この3点がごちゃごちゃしていて、意図した処理にならなかった。
    • exampleSeg1 をコピーでfilterAndSortSegmentsに渡す→新しいnewSegreturnする処理をしていた→main()側で代入してあげないとexampleSeg1 の中身は更新されなかった

ふりかえり (2周目、3/31)

  • std::boolalpha 0/1 の出力→ false/true に置き換えられる
  • 構造体(segment[2].tickなど)の値の出力で、range-based for文を使いながら添え字も欲しい
    • for文の外でint型変数を作って、出力のなかで idx++ を加える方法もある。
int idx = 0;
for (const auto& el : seg1) { //この書き方だと添え字が出てこない
    std::cout << idx++ << ": tick = " << el.tick 
              << ", enabled = " << std::boolalpha << el.enabled << std::endl;
}

第8問 座標変換 (3/17)

問題:二次元座標の変換プログラム

次の要件を満たすC++コンソールアプリケーションを作成してください。

  1. 入力
    • 最初に、整数 N を読み込む(座標ペアの数)。
    • つぎに、N 行にわたって (x, y) の整数座標ペアを読み込む。
    • その後、ドラッグオフセット (dx, dy) を読み込む。
    • 最後に、移動方向の制限を示す文字列を読み込む(”x” / “y” / “both” のいずれか)。
  2. 処理
    • 入力された座標ペアを順番に「ドラッグ」処理する。
      • もし制限が “x” の場合、y は変えずに x だけを (x + dx) に更新。
      • もし制限が “y” の場合、x は変えずに y だけを (y + dy) に更新。
      • もし制限が “both” の場合、(x, y)(x + dx, y + dy) に更新。
  3. 出力
    • 移動後の座標ペアを、N行にわたって表示する。
  4. サンプル実行例 (略)
  5. その他
    • 入力のバリデーション は大雑把でOK。
    • 出力の形式は、各座標ペアを1行ずつ表示するだけでよい。

ヒント:

  • C++で標準入力を扱うときは std::cin、標準出力は std::cout を使います。
  • 文字列比較は std::string を用いて == 演算子でOKです(C++11以降)。
  • 条件分岐(if / else if / else)で 制限モード (“x”,”y”,”both”) を振り分けるとよいでしょう。

この問題を解くことで

  • 標準入力 / 出力 の扱い
  • 配列やベクタを使った座標管理
  • 文字列に応じた分岐処理
  • ドラッグオフセットと座標変換
    などが身につきます。ぜひ挑戦してみてください!

最終解答

#include <iostream>
#include <vector>

int main()
{
    int N; //座標ペア
    std::cout<< "N: ";
    std::cin >> N;
    
    std::vector<std::vector<int>> pos(N,std::vector<int>(2));
    
    for(int i=0 ; i<N; ++i){
        std::cout << "pos(" << i << "): ";
        std::cin >> pos[i][0] >> pos[i][1];
    }
    
    int dx=0;
    int dy=0;
    std::cout << "(dx,dy): ";
    std::cin >> dx >> dy;
    
    std::string restriction;
    
    while(true){
        std::cout << "移動制限(x,y,both):";
        std::cin >> restriction;
        if (restriction == "x" || restriction == "y" || restriction == "both"){
            break;
        }
        
        std::cout << "エラー:x,y,bothのいずれかを入力\n";
    }

    //内部処理
    int dx_apply = (restriction == "y") ? 0 : dx;
    int dy_apply = (restriction == "x") ? 0 : dy;
    
    for(int i=0; i<N; ++i){
        pos[i][0] += dx_apply;
        pos[i][1] += dy_apply;
    }

    std::cout << "output:" << std::endl;
    for(int i=0; i<N; ++i){
        std::cout << "pos(" << i << "):" << pos[i][0] << " , " << pos[i][1] << std::endl;
    }

}

最終解答(2周目)

#include <iostream>
#include <vector>

int main()
{
    int N;
    std::cout<<"N: ";
    std::cin >> N;
    
    std::vector<std::vector<int>> point(N, std::vector<int>(2));
    
    //座標入力
    for(int i=0; i<N; ++i){
        std::cout << "i=" << i << ": ";
        int x;
        int y;
        std::cin >> x >> y;
        point[i][0] = x;
        point[i][1] = y;
    }

    //オフセット、移動制限
    int dx, dy;
    std::cout << "dx,dy: ";
    std::cin >> dx >> dy;
    
    std::string direct;
    while(true){
        std::cout << "移動方向: ";
        std::cin >> direct;
        
        if(direct=="x" || direct=="y" || direct=="both"){
            break;
        }
        
        std::cout << "x,y,bothのみ入力できます。" << std::endl;
    }
    
    //座標変換
    int apply_dx = direct=="y" ? 0 : dx;
    int apply_dy = direct=="x" ? 0 : dy;
    
    for(int i=0; i<N; ++i){
        point[i][0] += apply_dx;
        point[i][1] += apply_dy;
    }

    //座標出力
    for(int i=0; i<N; ++i){
        std::cout << "point[" << i << "]: ";
        for(int val: point[i]){
            std::cout << val << " ";
        }
        
        std::cout << std::endl;
    }

    return 0;
}

ふりかえり

  • 行24-32:入力ミス対策
    • だるいからはじめは処理させなかったが、ユーザーフレンドリーなコードとして重要
    • whileループに入る→入力指示→正しい入力があればbreak→ループの最後にエラー文 の流れが定石と考えられる(第6問と共通)
    • このへんの定石を集めてまとめて学べるといいなあ。知らないとすぐ書けない
  • 行34-41:座標変換のループ処理
    • もともと、restrictionの値ごとにfor文を3回書いていたが、dx_apply, dy_applyを三項演算子を使って定義することでシンプルに書ける
    • 三項演算子はMusescoreのコードを読んでいても頻出だったので現場では重要らしい

ふりかえり (2周目、3/31)

  • vectorの値の入れ方は次のいずれか
    • サイズを最初に決める → 添え字で入れる先を指定可能
    • サイズを決めない → push_back
  • ベクターの二次元配列で大きさを(N x 2)にする場合
    • std::vector<std::vector<int>> point(N, std::vector<int>(2));

まとめ

  • Hello world! を何度も出力するよりよほど実践的な訓練ができた。
  • ChatGPT(MEMちょ似)のいうことをどこまで信じてよいか測りかねる部分はあるが、コンパイラ通ってバグもないならとりあえず問題なさそう。
タイトルとURLをコピーしました