C++で文字列を1行ごとに処理する方法

以下の例のようにRange-based for loopを使用して処理できるようにします。

for (auto& line : lines("1,2,3\n4,5,6\n7,8,9")){
    cout << '[' << line << ']' << endl;
}

コード

using namespace std;

class LinesIterator : public iterator<input_iterator_tag, string>{
public:
    LinesIterator(istream& stream) :stream(&stream), isValid(!stream.eof()){
        ++(*this);
    }

    LinesIterator(){
    }

    const string& operator*() const { return line; }

    const string* const operator->() const { return &line; }

    iterator& operator++() {
        if (isValid && !getline(*stream, line)){
            isValid = false;
        }
        return *this;
    }

    bool operator==(const LinesIterator &x) const {
        return (!isValid && !x.isValid) || (this == &x);
    }

    bool operator!=(const LinesIterator &x) const { return !(*this == x); }

    static LinesIterator endItr;

private:
    string line;
    istream* const stream = nullptr;
    bool isValid = false;
};

LinesIterator LinesIterator::endItr;

class StreamLines{
public:
    StreamLines(istream& stream) :stream(stream) {
    }

    LinesIterator begin() {
        return LinesIterator(stream);
    }

    LinesIterator& end() const {
        return LinesIterator::endItr;
    }
private:
    istream& stream;
};

class StringLines : public StreamLines {
public:
    StringLines(const string& str) :ss(str), StreamLines(ss){
    }
private:
    istringstream ss;
};

StreamLines lines(istream& stream){
    return StreamLines(stream);
}

StringLines lines(const string& str){
    return StringLines(str);
}

int main(){
    for (auto& line : lines("1,2,3\n4,5,6\n7,8,9")){
        cout << '[' << line << ']' << endl;
    }
    cout << endl;
}

結果

[1,2,3]
[4,5,6]
[7,8,9]

std::(unordered_)mapにキーが入っているかどうかを判定する

stlのコンテナのfindメンバ関数は、条件に合うものが見つからない場合にendイテレーターを返します。

template <class M, class K>
bool containsKey(const M& map, const K& key){
    return map.find(key) != map.end(
}

使用例

if(containsKey(table, "hoge")){
  return table["hoge"];
}
return null;

フォルダ内の各ファイルをfor文で処理する

C++11ではfor文に新たな記法が追加され、便利になりました。 新しい記法ではイテレーターを返すbegin()とend()を持っているものなら何でも回すことができます。 イテレーターを自作することでfor文で書けるものを増やすことができます。

今回はDXライブラリの隠しAPIであるファイル検索APIイテレーターでラップしてみました。 あるフォルダ内で特定の拡張子を持つファイルを探す、といった処理が簡単に書けます。

for(FILEINFO& file : findFile("*.*")){
    printfDx("%s\t%d\n", file.Name, file.Size);
}

この例ではフォルダ内のすべてのファイルの名前とサイズを表示しています。 簡単ですね。

FILEINFOの部分はautoでもOKです。

for(auto& file : findFile("save/*.sav")){
    printfDx("%s\t%d\n", file.Name, file.Size);
}

saveフォルダ内のsavファイルを列挙。

昔のC++で書く場合は次のようにします。

FileSearchState state = findFile("save/*.sav");
for(FileSearchState::iterator& file = state .begin(); file != state.end(); ++file){
    printfDx("%s\n", file->Name);
}

大変ですね。

実装

#include <DxLib.h>
#include <iostream>
#include <vector>
#include <string>

class FileSearchState{
public:
    class iterator : public std::iterator<std::input_iterator_tag, std::string>{
    public:
        iterator(const char* path):isValid(false){
            if(path == nullptr){
                findhandle = -1;
                isValid = false;
            }else{
                findhandle = FileRead_findFirst(path, &fileinfo);
                isValid = (findhandle != -1);
            }
        }
        ~iterator(){
            if(findhandle != -1){
                FileRead_findClose(findhandle);
            }
        }
        const FILEINFO& operator*() const { return fileinfo; }
        const FILEINFO* operator->() const { return &fileinfo; }
        iterator& operator++() {
            isValid = (FileRead_findNext(findhandle, &fileinfo) != -1);
            return *this;
        }
        bool operator==(const iterator &x) const {
            return (isValid && x.isValid && findhandle == x.findhandle) || (!isValid && !x.isValid);
        }
        bool operator!=(const iterator &x) const { return !(*this == x); }
    private:
        FILEINFO fileinfo;
        int findhandle;
        bool isValid;
    };

    FileSearchState(const char* path){
        this->path = path;
    }

    iterator begin() const{
        return iterator(path);
    }

    iterator& end() const{
        static iterator endItr(nullptr);
        return endItr;
    }
private:
    const char* path;
};

FileSearchState findFile(const char* path){
    return FileSearchState(path);
}
FileSearchState findFile(const std::string path){
    return FileSearchState(path.c_str());
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow){
    ChangeWindowMode(true);
    SetMainWindowText("DxLibProject1");
    SetGraphMode(640, 480, 32, 60);

    if(DxLib_Init() == -1){
        return -1;
    }

    for(auto& file : findFile("*.*")){
        printfDx("%s\t%d\n", file.Name, file.Size);
    }

    WaitKey();

    DxLib_End();
    return 0;
}

フェードイン・フェードアウトを簡単に書けるFaderクラス

ゲームを作っているとある値を一定時間かけて変化させたいという場面が多くあります。 例えば、画面の明るさ(フェードイン・フェードアウト)、スコアの数値、HPゲージなど… こういった要素は一瞬で変化させるより、動きを与えた方が画面に高級感が出てきます。 しかしその都度値を管理するのはなかなかに面倒です。

こういう時はクラス化してしまうと便利になります。以下にその実装例を挙げました。 クラス定義が入っているので長いですが、最後のWinMain関数だけ見て下さい。

#include <DxLib.h>

class Timer{
public:
    Timer():time(0),count(0){};
    Timer(int time):time(time),count(0){};
    Timer(int time, int defaultCount):time(time),count(defaultCount){};
    ~Timer(){};
    void update(){ ++count; };
    void reset(){ count = 0; }
    void setTime(int time){ this->time = time; };
    void setTimeAndReset(int time){ this->time = time; count = 0; };
    int getCount() const { return count; };
    bool isPassed() const { return count >= time; };
private:
    int count;
    int time;
};

template<class T>
class Fader{
public:
    Fader():current(T()),goal(T()),velocity(T()){}
    Fader(const T& value):current(value),goal(value),velocity(T()){}
    const T& getCurrent() const { return current; }
    const T& getGoal() const { return goal; }
    void update(){
        if(timer.isPassed()){
            current = goal;
        }else{
            timer.update();
            current += velocity;
        }
    }

    operator T() const{
        return getCurrent();
    }

    const T& operator=(const T& value){
        set(value);
        return value;
    }
    T& operator=(T& value){
        set(value);
        return value;
    }
    void set(const T& value){
        current = goal = value;
        velocity = T();
        timer.setTimeAndReset(0);
    }
    void set(const T& value, int duration){
        if(value != goal){
            if(duration <= 0){
                return set(value);
            }
            goal = value;
            velocity = (goal - current) / duration;
            timer.setTimeAndReset(duration);
        }
    }
    bool isCompleted() const { return timer.isPassed(); }
private:
    T current;
    T goal;
    T velocity;
    Timer timer;
};

typedef Fader<double> NumberFader;


int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow){
    ChangeWindowMode(true);
    SetMainWindowText("FaderTest");
    SetGraphMode(640, 480, 32, 60);

    if(DxLib_Init() == -1){
        return -1;
    }

    SetDrawScreen(DX_SCREEN_BACK);

    NumberFader brightness = 0;
    // 明るさを240フレーム(4秒)かけて最大(255)にする
    brightness.set(255, 240 /*frames*/);

    while(ProcessMessage() == 0){
        ClearDrawScreen();
        DrawBox(0, 0, 640, 480, GetColor(brightness, brightness, brightness), true);
        brightness.update();
        ScreenFlip();
    }

    DxLib_End();
    return 0;
}

これだけです。明るさを定数にするのと比べてもほとんどコードを増やさずにフェードを実現できました。 なかなか便利ではないでしょうか?

C++で2次元ベクトルクラスを作る

ここでいうベクトルはstd::vectorではなく、2次元平面上の幾何ベクトルを意味します。 特徴は、a / b を外積にしている点です。これは当たり判定等で役立ちます。

#include <cmath>

class Vector2 {
public:
    double x, y;
    Vector2():x(0),y(0){
    }
    Vector2(const Vector2& v):x(v.x),y(v.y){
    }
    Vector2(double x, double y):x(x),y(y){
    }
    Vector2& operator = (const Vector2& v){
        x = v.x;
        y = v.y;
        return *this;
    }
    bool operator == (const Vector2& v) const{
        return (x == v.x && y == v.y);
    }
    bool operator != (const Vector2& v) const{
        return (x != v.x || y != v.y);
    }
    const Vector2& operator+() const{
        return *this;
    }
    Vector2& operator+(){
        return *this;
    }
    Vector2 operator-() const{
        return Vector2(-x, -y);
    }
    Vector2& operator+=(const Vector2& v){
        x += v.x;
        y += v.y;
        return *this;
    }
    Vector2 operator+(const Vector2& v) const{
        return Vector2(x + v.x, y + v.y);
    }
    Vector2& operator+(Vector2&& v) const{
        return v += *this;
    }
    Vector2& operator-=(const Vector2& v){
        x -= v.x;
        y -= v.y;
        return *this;
    }
    Vector2 operator-(const Vector2& v) const{
        return Vector2(x - v.x, y - v.y);
    }
    Vector2& operator-(Vector2&& v) const{
        v.x = -v.x;
        v.y = -v.y;
        v += *this;
        return v;
    }
    Vector2& operator*=(double k){
        x *= k;
        y *= k;
        return *this;
    }
    Vector2 operator*(double k) const{
        return Vector2(x * k, y * k);
    }
    Vector2& operator/=(double k){
        x /= k;
        y /= k;
        return *this;
    }
    Vector2 operator/(double k) const{
        return Vector2(x / k, y / k);
    }
    double operator*(const Vector2& v) const{
        return (x * v.x) + (y * v.y);
    }
    double operator/(const Vector2& v) const{
        return (x * v.y) - (y * v.x);
    }
    double length() const{
        return std::sqrt( (*this) * (*this) );
    }
    Vector2 unit() const{
        const double len = length();
        return len == 0 ? *this : *this / len;
    }
    double angle() const{
        return std::atan2(y, x);
    }
};

参考文献

C++でのCSVファイルの処理について

色々あさってみて、最終的にこれが今一番しっくりきたので載せます。せっかくだし。

data.csv

10, 20, 30  
hello, world, !    
12a, 23b, 34c 

main.cpp

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
using namespace std;

int main(){
    ifstream file("data.csv");
    vector<vector<string>> values;
    string str;
    int p;

    if(file.fail()){
        cerr << "failed." << endl;
        exit(0);
    }

    while(getline(file, str)){
        //コメント箇所は除く
        if( (p = str.find("//")) != str.npos ) continue;
        vector<string> inner;

        //コンマがあるかを探し、そこまでをvaluesに格納
        while( (p = str.find(",")) != str.npos ){
            inner.push_back(str.substr(0, p));

            //strの中身は", "の2文字を飛ばす
            str = str.substr(p+2);
        }
        inner.push_back(str);
        values.push_back(inner);
    }

    for(unsigned int i = 0; i < values.size(); ++i){
        for(unsigned int j = 0; j < values[i].size(); ++j){
            cout << values[i][j] << ",";
        }
        cout << endl;
    }

    return 0;
}

実行結果

10,20,30,  
hello,world,!,  
12a,23b,34c,  

という感じになります。
ちゃんと文字列も認識してくれてますね。

追記

さらにコードを整理しました。ただし、上記コードとは微妙に動作が違います。

#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <vector>
using namespace std;
 
vector<string> split(const string &str, const string &delim){
    vector<string> tokens;
    size_t cp, fp; // current position, found position
    const size_t dsize = delim.size();
    for(cp = 0; (fp = str.find(delim, cp)) != string::npos; cp = fp + dsize){
        tokens.emplace_back(str, cp, fp - cp);
    }
    tokens.emplace_back(str, cp, str.size() - cp);
    return tokens;
}

vector<vector<string>> parseTable(istream &stream, const string &delim){
    vector<vector<string>> table;
    string str;
    while(getline(stream, str)){
        table.push_back(split(str, delim));
    }
    return table;
}
 
vector<vector<string>> parseTable(const string &str, const string &delim){
    stringstream ss(str);
    return parseTable(ss, delim);
}
 
int main(){
    // stringをCSVとして解釈させる
    // ファイルから読み込む場合は、第1引数にファイルストリームを渡す
    // auto table = parseTable(ifstream("example.csv"), ",");
    auto table = parseTable("1,2,3\n4,5,6\n7,8,9", ",");
    for(auto& line : table){
        for(auto& cell : line){
            cout << cell << " ";
        }
        cout << endl;
    }
    cout << endl;
}

結果

1 2 3
4 5 6
7 8 9