29
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RFC4180準拠のCSV読み込みクラス

29
Last updated at Posted at 2016-03-18

2026年、GPT-5.2にこの記事を読ませてみる

この記事のCSV実装は、技術的には未完成であり、仕様理解も不十分である。

主な問題点は以下。

  • erase後のイテレータ無効化を考慮していない
  • RFC4180の引用符処理を完全に満たしていない
  • CRLF判定で境界チェックが不足している

特に erase の扱いは未定義動作の可能性がある。
この実装は「動く場合がある」コードであって、「安全なコード」ではない。
C++標準仕様の理解が浅いことが明確に露呈している。

待て待て待て凹む凹む凹む。

言い方言い方。もっとあれだ、過剰に恐縮しながら、婉曲に、俺に気を遣いながら、言いにくそうに、それでいて完璧に指摘しろ。

……か、かしこまりました。
差し出がましいようで大変恐縮ではございますが、
あくまで「より堅牢にするなら」という観点で、
恐る恐る申し上げます……

まず大前提としてあの記事は、

  • RFC4180をちゃんと読んでいる
  • CRLFと引用符の罠に気付いている
  • 「1文字ずつ見るしかない」と到達している

この時点で、実装思想はかなり正しいです。

方向は間違っていません。
むしろ真っ当です。

ただ……ほんの少しだけ……
① std::string::erase とイテレータの関係

恐れ入りますが……

str.erase(it); を実行した時点で、それ以降のイテレータは無効化される可能性がございます。

ふむふむ、なるほど、えーと。あ、ホントだ、これやべえ!
eraseしたらその後のイテレータ無効じゃん。実装系次第でほぼ期待通り動いてしまうけど、全く保証されてない。
it = str.erase(it);とかやんないといけないやつだわ。

よし、とりあえず上記三観点のみで原文をコメントアウトして理由を添えて修正してくれ。

かしこまりました。では……

以下、2016年の記事。

boostなにそれ美味しいの?

美味しいに決まってんだろが!

なんで入れてねえんだよ! 勝手にライブラリ入れちゃ駄目? マジか、お前の会社車輪の再発明しまくりなのかよ、おいふざけんなよboostだぞ。Qiitaとかに落ちてるソースじゃねえんだぞ?

これが業務系では稀に良くある事なのよ。いや、マジで。そんなバカな、そんなトコねえよって思うだろうが、あるんだよそれが。そういう客もいるんだよ。笑ってやるから社名言ってみろって? 言えるわけねえだろ。

そんでまた今時CSVかよ

あんな曖昧な方言だらけのフォーマット誰も使ってねえよ。嘘だよ、俺らの業界って宇宙の時間軸がGoogleさんとかとなんかズレてっから全然未だに現役なんだよな。バリバリ使われてんだよな。

で、どういうCSVなんだよ? 先頭TABはコメントとか臭いの入ってねえだろうな?

え? 他の会社からの提供物だから分からない? そうかよ、お前いっつもなんも分かんねえよな!

じゃあRFC4180に準拠するから後は勝手にやれ

そういう闇深い何かのせいで、CSVパーサーを作る羽目になってしまった。さあRFC準拠か。どれどれ。

ふむふむ。なんてこった。

誰だよこのRFC書いた奴は。ダブルクォートで囲ったらCRLF入れられるとか、それ標準にすんなよ。実装の事考えてねえだろ。これ真面目に実装してる奴っているの? シンプルさがウリなんじゃないのかCSVは。そこスポイルしてどーすんだよ。

えーとまとめると、

  • 各レコードの区切りはCRLF
  • ファイル末尾のCRLFは省略できる
  • 先頭行のヘッダはあってもなくてもよい
  • デリミタはコンマ
  • ダブルクォートで囲んでも囲まなくてもよい
  • ダブルクォートを含める場合はダブルクォートの前にダブルクォートを付加してエスケープさせる
  • CRLFもコンマもダブルクォートで囲めばセーフ

こんなトコか。
先頭文字が「#」だったらコメントだと思ってたけど、RFC4180では定義されてないんだな。っつーことはあれも方言だったのかよ。いやー今まで知らなんだ。そして知らなくても良かった知識。

コメントとかあったら、も一つ面倒になるとこだったよ。
例えば、

aaa,"b
# bb","ccc",

これの2行目がコメントの様に見えて実は b*[CRLF]*#bb とかいうデータだもんな。

"""" 
こういう奴とかもあるんだな。

CSVのくせにちょっと複雑とかふざけんなよ、楽しくなってきたじゃねえかよ。

つまり単純にCRLFで分割させるのはNG

結局1文字ずつ見てくしかないって事じゃん。
ほんでstd::vector<std::vector<std::string>>に突っ込んでしまえばいいんだろ、分かったよ。

しかしここでそう単純には行かない。(事もないんだけど)
極東の島国に住む我々ときたら、厚切りジェイソンに「Why Japanese people!」と絶叫されるおかしな言語を操る種族。2バイト文字があるんだよ、そうなんだよ、考慮すべきだよね。
今回はShift-JISって事でね。だからShift-JISでインクリメントするわけなんですよ。
MSだけなら_mbsincでも使えばいいじゃん(いいじゃん)って話なんだけど、一応MS方言は使わない方向でね。

別に2バイト目が5Cでも問題にならんから、考慮しなくてもいいじゃねーかって?

その通りなんだけどな。 ま、いいじゃん。折角だしさ。何が折角なんだ。

というわけで書いてみた(2026年2月ChatGPT-5.2による改訂)

csv_parser.hpp
# pragma once

# include <string>
# include <vector>
# include <fstream>

class csv_parser
{
public:
	const std::vector<std::vector<std::string>> &rows() {
		return rows_;
	}

	template <typename Function>
	inline void read(const std::string &file_name, Function strinc)
	{
		std::ifstream f(file_name, std::ios::binary);

		std::string str((std::istreambuf_iterator<char>(f)),
			std::istreambuf_iterator<char>());

		// [原文]
		// std::string::const_iterator it = str.begin();
		//
		// [修正]
		std::string::iterator it = str.begin();
		// [理由]
		// str.erase() を行うため const_iterator では不整合。
		// また erase 後は iterator が無効化され得るので、戻り値で更新できる iterator を使う。

		auto it_begin = it;

		while (it != str.end())
		{
			if (*it == '"')
			{
				it_begin = ++it;

				// [原文]
				// for (; !is_double_quotation(it, str);)
				// 	strinc(it);
				//
				// [修正]
				for (; it != str.end() && !is_double_quotation(it, str);)
					strinc(it);
				// [理由]
				// 引用符が閉じない入力でも境界を踏み抜かないための最低限の end チェック。
				// (指定の「RFC4180 引用符処理」の一環として、走査時の境界安全性を確保)

				// [原文]
				// str.erase(it);
				//
				// [修正]
				if (it != str.end())
					it = str.erase(it);
				// [理由]
				// erase 後に it を使い続けると iterator 無効化で未定義動作になり得るため。
				// erase の戻り値(次の有効 iterator)で it を更新する。
			}
			
			for (;;)
			{
				if (it == str.end())
				{
					push_field_with_row(it_begin, it);
					break;
				}
				if (is_delimiter(it))
				{
					push_field(it_begin, it);
					increment_delimiter(it);
					break;
				}

				// [原文]
				// if (is_crlf(it))
				// {
				// 	push_field_with_row(it_begin, it);
				// 	increment_crlf(it);
				// 	break;
				// }
				//
				// [修正]
				if (is_crlf(it, str))
				{
					push_field_with_row(it_begin, it);
					increment_crlf(it);
					break;
				}
				// [理由]
				// CRLF 判定で *(it + 1) を参照していたが、it が末尾の '\r' を指す場合に境界外参照。
				// (it + 1) != end のチェックを追加して安全化。

				strinc(it);
			}

			it_begin = it;
		}
	}

	inline void read(const std::string &file_name) {
		// [原文]
		// read(file_name, [](std::string::const_iterator &it){ ++it; });
		//
		// [修正]
		read(file_name, [](std::string::iterator &it){ ++it; });
		// [理由]
		// 上で iterator に変更したため型を合わせる(erase を行うので const_iterator のままでは不可)。
	}

private:
	std::vector<std::vector<std::string>> rows_;
	std::vector<std::string> fields_;

	// [原文]
	// void push_field_with_row(const std::string::const_iterator &_First, const std::string::const_iterator &_Last) {
	//
	// [修正]
	void push_field_with_row(const std::string::iterator &_First, const std::string::iterator &_Last) {
		// [理由]
		// iterator 型変更に追従(処理内容は同じ、指定観点以外は変更しない)。
		fields_.push_back(std::string(_First, _Last));
		rows_.push_back(fields_);
		fields_.erase(fields_.begin(), fields_.end());
	}

	// [原文]
	// void push_field(const std::string::const_iterator &_First, const std::string::const_iterator &_Last) {
	//
	// [修正]
	void push_field(const std::string::iterator &_First, const std::string::iterator &_Last) {
		// [理由]
		// iterator 型変更に追従(処理内容は同じ)。
		fields_.push_back(std::string(_First, _Last));
	}

	// [原文]
	// bool is_delimiter(const std::string::const_iterator &it) const {
	//
	// [修正]
	bool is_delimiter(const std::string::iterator &it) const {
		// [理由]
		// iterator 型変更に追従(判定ロジックは同じ)。
		return (*it == ',');
	}

	// [原文]
	// bool is_crlf(const std::string::const_iterator &it) const {
	// 	return ((*it == '\r') && (*(it + 1) == '\n'));
	// }
	//
	// [修正]
	bool is_crlf(const std::string::iterator &it, const std::string &str) const {
		// [理由]
		// CRLF 判定で it+1 の境界チェックが不足していたため追加。
		return (*it == '\r') && ((it + 1) != str.end()) && (*(it + 1) == '\n');
	}

	// [原文]
	// void increment_delimiter(std::string::const_iterator &it) const {
	//
	// [修正]
	void increment_delimiter(std::string::iterator &it) const {
		// [理由]
		// iterator 型変更に追従。
		++it;
	}

	// [原文]
	// void increment_crlf(std::string::const_iterator &it) const {
	//
	// [修正]
	void increment_crlf(std::string::iterator &it) const {
		// [理由]
		// iterator 型変更に追従。
		it += 2;
	}

	// [原文]
	// bool is_double_quotation(const std::string::const_iterator &it, std::string &str) const {
	// 	if (*it == '"') {
	// 		if ((it + 1) == str.end())
	// 			return true;
	//
	// 		if (*(it + 1) != '"')
	// 			return true;
	// 		else
	// 			str.erase(it);
	// 	}
	// 	return false;
	// }
	//
	// [修正]
	bool is_double_quotation(std::string::iterator &it, std::string &str) const {
		if (*it == '"') {
			if ((it + 1) == str.end())
				return true;

			if (*(it + 1) != '"')
				return true;
			else {
				// [理由]
				// RFC4180 の ""(引用符のエスケープ)を正しく処理する:
				//  - 連続する2つの " はデータ上は 1つの " として扱う
				//  - 先頭の " を erase して "" を " に正規化する
				//  - erase 後は iterator が無効化される可能性があるので戻り値で it を更新する
				//  - 残った "(データの1文字)を消費するために ++it する
				it = str.erase(it);
				++it;
			}
		}
		return false;
	}
};

main関数がこれね。

main.cpp
# include "csv_parser.hpp"
# include <iostream>

int main(int argc, char* argv[])
{
	csv_parser parser;

	parser.read<>("test.csv", [](std::string::const_iterator &it) {
		unsigned char c = *it;
		it += (((c >= 0x81) && (c <= 0x9f)) || ((c >= 0xe0) && (c <= 0xfc))) ? 2 : 1; });

	for each(auto row in parser.rows())
	{
		std::cout << "-------" << std::endl;
		for each(auto r in row)
			std::cout << " * " << r << std::endl;
	}

	return 0;
}

一応readの第2引数でShift-JIS用インクリメントをしてんだけど、まあ別にいらなければいらないかも。

parser.read("test.csv");

これだけでも動くんだけどね。
いいのよ、隙あらばラムダ って事ですよ。お前はホントにラムダが必要だったのかと小一時間問い詰められても仕方ないです。
template <typename Function>書きたかっただけちゃうんか、と。
そう言われたら返す言葉もございません。
そうさ、書きたかっただけさ。

まあ100行ちょいで

そこそこちゃんと動いてるのかな。EUCとかUTF8も大体いけるやろ(適当)
インクルードガードを #pragma once で書いてるから気をつける人は気をつけてね。俺はVCとgccで通れば満足だからifndefとかしねーよ、へへへん。

makeいらずでhppだけincludeすれば使えるんで

お手軽かなーと。現実に使う場合は異常系とかしっかりさせないとダメなんだけど、取り敢えずこれでいいや。

そんでboost入れさせてない企業の人、これを見てたら速攻悔い改めるように。

というわけで2026年追記

この記事は、結果的に「実績のある既存コードを使わずに自前実装することのリスク」を炙り出すことになった。
イテレータを連続領域のポインタのように扱っている点は汗顔の至りである。

しかし同時に、これはこの10年で我々の開発環境がいかに変遷したかを示すログでもある。

2026年現在、AIの補助なしにこの種のコードを自力で書くことは、明確にハイリスクである。
さらに言えば、標準規格の細部を人間が逐一暗記・保持すること自体の意味も相対的に希薄になりつつある。

私自身、今では Claude code や Codex CLI、Antigravity を主戦場に投入している。
だがそれは、思考停止の委譲ではない。

むしろ今回の検証を通じて再確認したのは、「よりローリスクな技術に常にキャッチアップし続けること」こそが、時代が変わっても変わらない本質であるという点だ。(文責:ChatGPT-5.2)

※プロンプト
「おい、なんか俺の顔を潰さない程度に、いい感じで前向きな文章考えろ」

29
23
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
29
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?