[C++] binary_search 정리

메모리 기반의 방대한 데이타가 있다고 가정을 할 때.. 이 데이터 목록 중 어떤 데이타가 존재하는지의 여부를 가장 빠르게 검색해주는 방식이 binary_search인데요. 이 STL 함수를 정리해 봅니다.

먼저 메모리 기반의 방대한 데이터를 준비해 봅니다.

#include <iostream>
#include <list>
#include <algorithm>
#include <iterator>

using namespace std;

int main()
{
    list<int> values{ 1, 2, 8, 7 ,6, 5, 4, 1, 9, 3, 5, 6, 7 };

}

데이타가 몇개 되지 않지만 방대하다고 칩시다. binary_search를 사용하기 위해서는 먼저 데이터가 정렬되어 있어야 합니다. 내림차순으로 정렬하는 코드를 추가하면.. 다음과 같습니다.

#include <iostream>
#include <list>
#include <algorithm>
#include <iterator>

using namespace std;

int main()
{
    list<int> values{ 1, 2, 8, 7 ,6, 5, 4, 1, 9, 3, 5, 6, 7 };
    auto predicate = [](int a, int b) { return a > b; };
    values.sort(predicate);

    copy(values.begin(), values.end(), ostream_iterator<double>{ std::cout, " "});
    cout << endl;
}

람다 함수로 내림차순 정렬을 위한 기준으로 삼았습니다. 그리고 잘 정렬되었는지 콘솔에 표시해 보았구요. 이제 binary_search 함수를 사용하기 위한 데이터 덩어리가 준비 되었고, 다음처럼 데이터 덩어리중 값 5가 존재하는지의 여부를 검색하는 코드는 다음과 같습니다.

#include <iostream>
#include <list>
#include <algorithm>
#include <iterator>

using namespace std;

int main()
{
    list<int> values{ 1, 2, 8, 7 ,6, 5, 4, 1, 9, 3, 5, 6, 7 };
    auto predicate = [](int a, int b) { return a > b; };
    values.sort(predicate);

    copy(values.begin(), values.end(), ostream_iterator<double>{ std::cout, " "});
    cout << endl;

    int wanted{ 5 };
    bool bSearched = binary_search(begin(values), end(values), wanted, predicate);
    if (bSearched) {
        cout << "Found !" << endl;
    } else {
        cout << "Not found." << endl;
    }
}

[C++] Stream을 이용한 Typed Binary 데이터 읽고 쓰기

C++은 stream을 통해 반복자(iterator)를 이용해 데이터를  매우 유연하게 처리 할 수 있습니다. 이 스트림을 통해 데이터의 입력을 키보드나 파일 등을 통해 매우 유연하고 효과적으로 읽고 쓸 수 있는데요. 문제는 일반화된 문자열에 대한 읽고 쓰기에 중심을 두고 있다는 점입니다.  즉,  C++의 스트림을 이용해 실수 타입의 값으로 3.14159265라는 값을 파일에 저장하고자 한다면, 이 값을 메모리에 저장된 그대로의 실수형 바이너리 형식으로의 저장이 아닌 “3.14159265”라는 문자열로 저장되는 것이 C++ 개발자에게 흔히 노출된 방식이라는 것입니다.

다 이유가 있겠으나, 필자는 이 실수형 값을 메모리에 저장된 그대로의 실수형 바이너리 형식으로 파일에 저장하고, 이렇게 저장된 값을 다시 파일을 열어 꺼내와 화면에 표시하고자 합니다.

먼저 3.14159265 값을 d 드라이브의 data.bin에 저장하는 코드입니다.

#include <iostream>
#include <fstream>

int main()
{
	std::ofstream os("d:\\data.bin", std::ios::binary);

	double v = 3.141592625;
	os.write(reinterpret_cast<const char*>(&v), sizeof(double));

	os.close();
}

실행해 보면 data.bin 파일이 생성되어졌고, 파일 크기는 double의 바이트 크기와 동일한 8바이트인 것을 확인할 수 있습니다.

이제 이렇게 생성된 파일에서 다시 실수형 값을 읽어 보는 코드는 다음과 같습니다.

#include <iostream>
#include <fstream>

int main()
{
	std::ifstream is("d:\\data.bin", std::ios::binary);

	double v2;
	is.read(reinterpret_cast<char*>(&v2), sizeof(double));
	
	is.close();

	std::cout << v2;
}

여기서 C++의 template을 이용해 개선을 해보도록 하겠습니다. 즉, 다음과 같은 2개의 템플릿 함수를 추가합니다.

template<typename T>
std::ostream& write_typed_data(std::ostream& stream, const T& value) {
	return stream.write(reinterpret_cast<const char*>(&value), sizeof(T));
}

template<typename T>
std::istream & read_typed_data(std::istream& stream, T& value) {
	return stream.read(reinterpret_cast<char*>(&value), sizeof(T));
}

네, write_typed_data와 read_typed_data는 각각 임이의 데이터 타입에 대해서 지정된 스트림에 메모리에 저장된 그대로의 구조인 바이너리 형식으로 쓰고 읽는 범용 함수입니다.

이 두 함수를 활용해 3.141592625 값을 쓰고 읽는 전체 코드를 다시 작성해 보면 다음과 같습니다.

#include <iostream>
#include <fstream>

template<typename T>
std::ostream& write_typed_data(std::ostream& stream, const T& value) {
	return stream.write(reinterpret_cast<const char*>(&value), sizeof(T));
}

template<typename T>
std::istream & read_typed_data(std::istream& stream, T& value) {
	return stream.read(reinterpret_cast<char*>(&value), sizeof(T));
}

int main()
{
	// Write 
	std::ofstream os("d:\\data.bin", std::ios::binary);

	double v = 3.141592625;
	//os.write(reinterpret_cast<const char*>(&v), sizeof(double));
	write_typed_data(os, v);

	os.close();
	// <- test unit end

	// Read
	std::ifstream is("d:\\data.bin", std::ios::binary);

	double v2;
	read_typed_data(is, v2);
	//is.read(reinterpret_cast<char*>(&v2), sizeof(double));
	
	is.close();

	std::cout << v2;
	// <- test unit end
}

C++에서 스트림을 활용해 타입을 가지는 변수를 바이너리 형식으로 저장하는 위의 방식이 정석인지는 모르겠습니다. (이제와서 이게 무슨 소리? -_-;) reinterpret_cast를 사용했다는 점에서 상당히 의구심이 들기 때문인데요. 더욱 나은 방식이 있다면 제안해 주시기 바랍니다. reinterpret_cast는 포인터가 다른 포인터 형식으로 변환될 수 있도록 하거나 정수 계열 형식이 포인터 형식으로 변환될 수 있도록 하고 그 반대로도 변환될 수 있도록 하는 cast 연산자입니다.

[C] 파일을 포함하는 디렉토리 삭제

하위 디렉토리나 하위 파일을 포함하는 디렉토리를 삭제하는 함수입니다.

int DeleteAllFiles(LPCWSTR szDir, int recur)
{
    HANDLE hSrch;
    WIN32_FIND_DATA wfd;
    int res = 1;

    TCHAR DelPath[MAX_PATH];
    TCHAR FullPath[MAX_PATH];
    TCHAR TempPath[MAX_PATH];

    lstrcpy(DelPath, szDir);
    lstrcpy(TempPath, szDir);
    if (lstrcmp(DelPath + lstrlen(DelPath) - 4, _T("\\*.*")) != 0) {
        lstrcat(DelPath, _T("\\*.*"));
    }

    hSrch = FindFirstFile(DelPath, &wfd);
    if (hSrch == INVALID_HANDLE_VALUE) {
        if (recur > 0) RemoveDirectory(TempPath);
        return -1;
    }

    while(res) {
        wsprintf(FullPath, _T("%s\\%s"), TempPath, wfd.cFileName);

        if (wfd.dwFileAttributes & FILE_ATTRIBUTE_READONLY) {
            SetFileAttributes(FullPath, FILE_ATTRIBUTE_NORMAL);
        }

        if (wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
            if (lstrcmp(wfd.cFileName, _T(".")) 
                && lstrcmp(wfd.cFileName, _T(".."))) {
                recur++;
                DeleteAllFiles(FullPath, recur);
                recur--;
            }
        } else {
            DeleteFile(FullPath);
        }

        res = FindNextFile(hSrch, &wfd);
    }

    FindClose(hSrch);

    if (recur > 0) RemoveDirectory(TempPath);

    return 0;
}

성공하면 0을 반환하고 그렇지 않다면 -1을 반환. 인자인 recur이 1이면 szDir로 지정된 디렉토리 자체도 삭제하고 0이라면 szDir로 지정된 디렉토리 자체는 삭제하지 않습니다. 참고로 이 함수는 제가 만드거 아니고 인터넷에서 찾은 것인데 출처를 몰라 =_=; 여튼, 좋은 함수 제공해 주셔서 감사합니다 !

[C++11] decltype

decltype은 주어진 표현식에 대한 결과의 타입을 컴파일러가 추론하라는 키워드(keyword)입니다. 예를 들어서 다음의 코드를 보면..

#include "stdafx.h"
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    cout << typeid(decltype(10)).name() << endl;

    return 0;
}

위 코드에서 8번을 보면 decltype(10)이라는 코드가 있는데, 표현식 10에 해당하는 type을 컴파일러가 추론을 하여 해당 타입의 이름을 출력하게 되는 것으로 결과는 'int'를 출력합니다.

또 다른 예를 살펴보면..

#include "stdafx.h"
#include 

using namespace std;

auto func(int a, int b) -> decltype(a + b)
{
    return a + b;
}

int _tmain(int argc, _TCHAR* argv[])
{
    cout << func(100, 200) << endl;

    return 0;
}

6번 코드를 보면 함수의 결과값에 auto를 사용할 수 없음에도 func라는 함수의 반환값이 auto를 사용하고 있습니다. 이것이 가능한 이유는 바로 함수의 정의 뒤에 오는 -> decltype(a+b)에 의해서 입니다. 즉, a+b에 대한 결과값의 타입을 추론하여 ->에 의해 함수의 반환값 추론에 대한 힌트를 제공하는 것입니다. a와 b는 int 타입이고 이 int 값들의 합 역시 int이므로 쉽게 함수의 결과값의 타입은 int라는 것을 추론할 수 있는 것입니다.