[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라는 것을 추론할 수 있는 것입니다.

[C++11] 람다(Lambda) 표현식

C++11은 람다 표현식 기능을 제공합니다. 람다 표현식은 함수를 미리 정의하지 않고 필요한 시점에서 사용하고 바로 버리는 것으로 생각할 수 있습니다. 람다가 제공되기 이전에 어떤 함수를 사용하고자 했다면, 그 함수를 미리 정의하고 사용하게 됩니다. 사용하고 난 뒤에 그 함수는 계속 존재합니다.아래는 람다 표현식을 이용해 만들어진 함수 예입니다.

#include "stdafx.h"
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    auto func = []() {
        cout << "Hello World" << endl;
    };

    func();

    return 0;
}

8번 코드에서 람다 표현식을 이용해 함수를 정의하고 있습니다. 정의된 함수는 예제처럼 어떤 변수에 함수를 할당해 놓고 12번 코드에서 실행할 수 도 있지만 어떤 변수에 할당하지 않고 바로 실행할 수 도 있습니다. 즉, 아래처럼 말입니다.

#include "stdafx.h"
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    []() {
        cout << "Hello World" << endl;
    }();

    return 0;
}

첫번째 예제와는 다르게 이 두번째 예제는 필요한 시점에 함수를 정의하고 바로 실행하고 함수를 폐기하고 있습니다. 8번 코드에서 보는 것처럼 람다 표현식은 2가지로 구성됩니다. []과 {}이며 []는 람다 소개자(Lambda Introducer) 또는 Capture Clause라고 하며 {}는 람다 몸체(Lambda Body)입니다. 람다 몸체에 함수 구현 코드가 존재하며 람다 소개자에 외부 변수를 어떤 식으로 참조할지에 대한 Capture 지정자과 함수에 입력될 인자(Parameter) 리스트가 지정됩니다. 예를 들어 람다 표현식으로 정의한 함수의 인자로 int를 받아 출력하는 함수는 다음과 같습니다.

#include "stdafx.h"
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    [](int a) {
        cout << a << endl;
    }();

    return 0;
}

이제 람다 함수(람다 표현식으로 정의된 함수)에 대해 몇가지 예를 통해 좀더 정리해 보겠습니다. 다음 코드를 살펴 보겠습니다.

#include "stdafx.h"
#include 
#include 
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    vector v { 1, 2, 3, 4, 5 };

    for_each(v.begin(), v.end(), [](int v) {
        cout << v << endl;
    });

    return 0;
}

위의 코드 중 12번의 for_each 함수는 컨테이너의 각 요소를 인자로 어떤 함수(또는 함수객체)를 호출하는 것으로 for_each의 세번째 인자가 바로 호출되는 함수(또는 함수객체)가 됩니다. 일반적으로 호출할 함수를 미리 정의해 두어야 하나 여기서는 람다 함수를 이용해 미리 함수를 정의하지 않고 필요한 시점에 함수를 정의하고 사용하고 있습니다. 다음 예제를 보겠습니다.

#include "stdafx.h"
#include 
#include 
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    vector v { 1, 2, 3, 4, 5 };

    auto it = find_if(v.begin(), v.end(), [](int v)->bool {
        return (v % 2) == 1;
    });

    cout << *it << endl;

    return 0;
}

위의 코드 중 12번에 있는 find_if 함수는 컨테이너를 구성하는 요소 중 어떤 조건을 만족하는 첫번째 요소의 값을 반환하는 함수인데, 조건을 지정하기 위해 함수 또는 함수 객체를 세번째 인자에 지정됩니다. 여기서는 람다 함수를 통해 조건 함수가 지정되었는데 이 함수의 반환 타입(Type)을 지정하기 위해서 ->bool를 사용함으로써 반환 타입이 bool이라고 명시하고 있습니다. 엄격하게는 이 ->bool 코드를 생략할 수 있는데, 이는 컴파일러가 이 람다함수의 반환타입을 추론할 수 있기 때문입니다. 컴파일러의 추론 대신 직접 반환 타입을 알려주기 위해 ->(type) 연산자를 사용할 수 있다는 것에 대한 예제입니다.

#include "stdafx.h"
#include 
#include 
#include 

using namespace std;

int _tmain(int argc, _TCHAR* argv[])
{
    vector v { 1, 2, 3, 4, 5 };
    int sum = 0;

    for_each(v.begin(), v.end(), [&](int v) {
       if (v % 2 == 0) sum += v;
    });
    
    cout << sum << endl;

    return 0;
}

위 코드 중 13번의 람다 함수를 보면 []에 해당하는 람다소개자의 형태가 [&]처럼 되어 있습니다. 이 [&]의 의미는 람다 함수 앞단에 모든(All) 외부 변수를 참조 타입(Reference Type)으로 잡아(Capture) 사용하겠다는 의미입니다. 실제 람다 함수 구현부를 보면 외부에 존재하는 sum 변수를 사용하고 있습니다. 참조 형태로 사용하겠다고 했으므로 람다 함수 내부에서 변수값을 변경하면 외부에서도 변경되는 것을 알 수 있습니다. 람다 소개자가 []일 경우에는 어떤한 외부 변수도 사용하지 않겠다는 의미이며 [&]는 모든 외부 변수를 참조로 사용하겠다는 것이고, [=]는 모든 외부 변수를 상수값(const)으로 값 자체를 복사(Copy)하여 사용하겠다는 의미입니다. [&a]는 외부 변수 중 a만을 참조값으로 사용하겠다는 의미이고 [&a, =b]는 a는 참조값으로 b는 상수값으로 사용하겠다는 의미입니다.