中年engineerの独り言 - crumbjp

LinuxとApacheの憂鬱

VC++がC++じゃ無くなってた。。

CLI/C++入門みたいなモノ

PCに溜まりに溜まっていた未整理の、バックアップやらコピーやらしまくった
デジカメ画像共を整理すべく、久しぶりにVC++(2008)を弄ってみた。

VCはVC6がどセンターで2003年辺りに弄った以来で、おぼろげな記憶しか残ってないのだけど、
なんとかなると思っていた・・・

そう『C++/CLI』を知るまでは!!

CLI

実はCLI自体がどういう物かは知っていました。

こんな認識でした

  • VB,C#,VC++などで共通に使えるプラットフォーム上の機能。
  • 実体は.NET上に存在し各言語は.NETへの参照を取り扱う。
  • C++から見た場合はメモリー周りを気にしなくて良い楽チンなオブジェクト!
  • C++の貧乏臭い文字列処理能力から開放されるぜぃ〜

知らなかったのは
凶悪なVC++シンタックス!!

百聞は一見にしかずというのはこの事だ!

  String^ foo = gcnew String("FOO");

oops!!
C++じゃないよ・・・
いやC++と名乗っちゃイカンだろ!これは!!

C++も書けるCLI言語だなこれは。。

しかし敢えて言おう!
これで良い!
C++は非常に曖昧で難解な識別子が多く、そこに更に新しい要素(CLI)を加えてしまっては最早ナゾナゾになってしまう。

CLI入門

VC++2008のExpressEdition(無償)を使っています。

ビルドオプション

まずCLIを使うにはこの設定が必要。
[プロジェクト・プロパティ]
 ⇒[全般]
 ⇒[共通言語ランタイムサポート]
 = "/clr"
または、プロジェクトを新規作成する際にCLIプロジェクトが選択できるのでそちらでも良い。

CLIの型とオブジェクト

型にはハット"^"が付く。(ポインタのような使用感)
オブジェクト化にはgcnew を使う。

 String^ foo = gcnew String("FOO");
CLI型の定義

ref を付けます。

ref struct Foo {
}; 
継承

C++同様に継承できますが、override , new を明示的に指定しなければならない。

ref struct Foo {
 virtual void foo() {}
 virtual void bar() {}
}; 
ref struct Bar : Foo{
 virtual void foo() override {}
 virtual void bar() new {}
}; 
Foo ^foo = gcnew Bar();
foo->foo();         // Bar::foo()
foo->bar();         // Foo::bar()
((Bar^)foo)->bar(); // Bar::bar()

overrideがC++の動作です。newは使いにくそう・・・

純粋仮想関数は扱いが面倒

ref struct Foo {
 virtual void baz() =0;
};
ref struct Bar : Foo {
 virtual void baz(){}
};
>> 'Foo' : 明示的に抽象として宣言されていませんが、抽象関数を含んでいます
>> 'Bar::baz' : 基本 ref クラス メソッド 'Foo::baz' と一致しますが、
>> 'new' または 'override' に設定されていません。'new' (および 'virtual') を仮定します

Fooには abstruct を宣言して、Bar::bazにはoverrideを指定する!

ref struct Foo abstract{
 virtual void baz() =0;
};
ref struct Bar : Foo{
 virtual void baz() override {}
};
混ぜるな危険!(混ざりません)

CLI型はフレームワーク側で管理されているので
通常のC++クラスのメンバーとなったり、継承関係となったりが出来ません。

struct Baz {
  Foo ^foo;
}
>> マネージ 'foo' をアンマネージ 'Baz' で宣言できません。
ref struct Foo {
};
struct Baz : Foo{
};
>> 'Baz': アンマネージ型はマネージ型 'Foo' から派生することはできません。

わざわざ書かないけど、当然逆もダメ!

cliextコンテナ

stdコンテナは『混ぜるな危険!』の為、使い物になりません。
その代わりstdライクなコンテナが用意されてます。

#include <cliext/map> 
#include <cliext/set> 
using namespace cliext;
using namespace System;
typedef map<String^,String^ > mymap_t;
typedef set<String^> myset_t;

ただ使い方が微妙に違います。

  mymap_t ^mymap = gcnew mymap_t();
  mymap->insert(mymap_t::value_type(key,value));
>>'Microsoft::VisualC::StlClr::GenericPair<TValue1,TValue2> ^' : 
>> 関数スタイルをビルトイン タイプへ変換すると、受け取ることができる引数は 1 つです。

何を言ってるか解らなかったが、、どうもmake_valueって奴を使うらしい・・・
じゃあ value_type定義すんなよ!混乱するじゃんかー

  mymap_t ^mymap = gcnew mymap_t();
  mymap->insert(mymap_t::make_value(key,value));
  for ( mymap_t::iterator it(mymap->begin()),itend(mymap->end());
		it != itend;
		it++){
    it->first;  // key
    it->second; // value
  }

後はいつもの感じ!

で、、成果発表

仕様

いきなり消すと怖いから、batファイル出力にしておいた。
でもこの慎重さでコマンドプロンプトのUTF8問題に嵌る事になる・・・

UTF8 on コマンドプロンプト問題
  • コマンドプロンプトのデフォルト文字コードはCP932
  • よってUTF8でbatを吐き出すと実行時に文字化けする
  • かといってCP932に変換するのは面倒
  • "chcp 65001"でコマンドプロンプト自体はUTF8になるがフォントと連動しない
  • 処理できないフォントが出力されると『場合によってはcmdが落ちる!

文字が出ないのは良いが、落ちるのは困る。。
思い悩んだ挙句何も出力しない事にした。

ソース
#include "stdafx.h"
#include <cliext/map> 
#include <cliext/set> 
using namespace cliext;
using namespace System;
using namespace System::IO;
using namespace System::Security::Cryptography;

typedef map<String^,String^ > filemap_t;
typedef map<String^,String^ > dubdmap_t;
typedef set<String^> dirset_t;


ref struct Callback abstract{
	virtual void file(String ^file) = 0;
	virtual void dir(String ^dir) = 0;
};


void folder_walk(String ^folder,Callback ^callback){
	try {
		array<String^>^ file = Directory::GetFiles( folder );
		Console::WriteLine(" - Checking {0}", folder);
		int i;
		for (i=0; i<file->Length; i++){
			callback->file(file[i]);
		}
		array<String^>^ dir = Directory::GetDirectories( folder );
		for (int i=0; i<dir->Length; i++){
			folder_walk(gcnew String(dir[i]),callback);
			callback->dir(dir[i]);
		}
	}catch(IOException ^ex){
		Console::WriteLine("'{0}'", ex->Message );
	}
}

int pre_check(String ^folder){
	try {
		array<String^>^ dir = Directory::GetDirectories( folder );
	}catch(IOException ^ex){
		Console::WriteLine("{0}", ex->Message );
		return 0;
	}
	return 1;
}

void check(String ^folder,Callback ^callback){
	folder_walk(folder,callback);
}


ref struct OrgCallback : Callback{
	filemap_t ^filemap;
	dubdmap_t  ^dubdmap;
	StreamWriter^ pwriter;
	OrgCallback(String ^log)
		:filemap(gcnew filemap_t()),
		dubdmap(gcnew dubdmap_t()),
		pwriter(gcnew StreamWriter(log))
	{
	}
	virtual void file(String ^file) override{
	   try{
		   FileStream^ fs = gcnew FileStream(
			   file,
			   FileMode::Open,
			   FileAccess::Read,
			   FileShare::Read);
		   MD5^ md5 = gcnew MD5CryptoServiceProvider();
		   array<Byte> ^hash = md5->ComputeHash(fs);
		   fs->Close();
		   String ^md5str = BitConverter::ToString(hash)->ToLower()->Replace("-","")->ToString();
		   pwriter->WriteLine(md5str + " " + file);
		   filemap_t::iterator it = filemap->find(md5str);
		   if ( it == filemap->end()) {
			   // not found !
			   filemap->insert(filemap_t::make_value(md5str,file));
		   }else{
			   // Doubled
			   dubdmap->insert(dubdmap_t::make_value(file,it->second));
			   // Console::WriteLine(" *** Same file !! {0}  == {1}'", file,it->second);
		   }
	   }catch(IOException ^ex){
		   (void)ex;
		   // nothing to do
	   }
	}
	virtual void dir(String ^dir) override{
	}
	~OrgCallback(){
		pwriter->Close();
	}
};

ref struct RmdirCallback : Callback{
	virtual void file(String ^file) override{
		File::Delete(file);
	}
	virtual void dir(String ^dir) override{
		Directory::Delete(dir,true);
	}
};


void usage(int ret){
	Console::WriteLine("Usage: DoubleFileChecker.exe Directory [Directory] ..");
	Threading::Thread::Sleep(5000);
	Environment::Exit(ret);
}

int main(array<String ^> ^args)
{
	if ( args->Length < 1 ) {
		usage(1);
	}

	// PreCheck
	Console::WriteLine("====== Precheck =======");
	for ( int i = 0 ; i < args->Length; i++ ){
		String^ folder = gcnew String(args[i]);
		if ( ! pre_check(folder) ) {
			usage(1);
		}
	}
	
	// Temp directory
	String ^tmpDir = Path::GetTempPath() + "_DFC_\\";
	RmdirCallback ^rmdirCallback = gcnew RmdirCallback();
	folder_walk(tmpDir,rmdirCallback);
	Directory::CreateDirectory(tmpDir);
	String^ org = (gcnew String(args[0]))->Replace("\\","_")->Replace(":","");
	String^ hashLog = tmpDir + org + ".log";
	String^ dubdBat = tmpDir + org + ".bat";
	String^ dirsBat = tmpDir + org + "_dir.bat";

	// Check
	Console::WriteLine("  OK ! => (log) : " + hashLog);
	OrgCallback ^callback = gcnew OrgCallback(hashLog);
	for ( int i = 0 ; i < args->Length; i++ ){
		String^ folder = gcnew String(args[i]);
		Console::WriteLine("====== Run check =======");
		Console::WriteLine("TOP  : " + folder);
		check(folder,callback);
	}

	// Gen batches
	Console::WriteLine("====== Generating results =======");
	Console::WriteLine("Delete files : " + dubdBat);
	Console::WriteLine("Cleanup dirs : " + dirsBat);

	StreamWriter ^bwriter = gcnew StreamWriter(dubdBat);
	bwriter->WriteLine("echo off");
	bwriter->WriteLine("md " + tmpDir);
	bwriter->WriteLine("rem  use UTF-8");
	bwriter->WriteLine("chcp 65001");

	dirset_t ^dirs = gcnew dirset_t();
	for ( dubdmap_t::iterator it(callback->dubdmap->begin()),itend(callback->dubdmap->end());
		it != itend;
		it++){
			bwriter->WriteLine("rem " + it->first + " same as " + it->second );
			String ^srcdir = Path::GetDirectoryName(it->first);
			String ^dstdir = tmpDir + srcdir->Replace(":","") + "\\";
			if ( dirs->find(srcdir) == dirs->end() ) {
				dirs->insert(srcdir);
				bwriter->WriteLine("md \"" + dstdir + "\" > NUL 2>&1");
			}
			bwriter->WriteLine("move \"" + it->first + "\" \"" + dstdir + Path::GetFileName(it->first) + "\" > NUL 2>&1");
	}
	bwriter->Close();

	StreamWriter ^dwriter = gcnew StreamWriter(dirsBat);
	dwriter->WriteLine("echo off");
	dwriter->WriteLine("md " + tmpDir);
	dwriter->WriteLine("rem  use UTF-8");
	dwriter->WriteLine("chcp 65001");
	for ( dirset_t::iterator it(dirs->begin()),itend(dirs->end());
		it != itend;
		it++){
			dwriter->WriteLine("del /F /AH \"" + *it + "\\Thumbs.db\" > NUL 2>&1");
			dwriter->WriteLine("rd \"" + *it + "\" > NUL 2>&1");
	}
	dwriter->Close();
	Threading::Thread::Sleep(5000);
	return 0;
}
PS:
  • ソースを微修正
  • 頑張ったけど、やっぱ.batは貧弱すぎて対応できないファイル名もある模様・・・{}辺りが怪しいかな?