2016년 3월 23일 수요일

Effective Modern C++, Item2: auto의 타입 추론을 이해하자

auto의 타입 추론은 템플릿의 그것과 동일하다. 하지만 이게 어떻게 가능한 것일까?

물론 사용 방법은 다르지만 템플릿 타입 추론과 auto 타입 추론은 직접적으로 연결이 되어있다.

말 그대로 변형 알고리즘을 이용하여 하나에서 다른 하나로 바꾸는 것이다.


auto x = 27;
const auto cx = x;
const auto& rx = x;

item1의 템플릿의 param의 타입과 굉장히 흡사하지 않는가? 여기서 타입을 결정하는 것은 auto와 그 주변에 붙어있는 것들이다.

컴파일러는 위와 같은 코드를 마치 템플릿이 사용된 것 처럼 이해한다. 다시 말해 아래와 같은 코드로 바꿀 수 있다는 말이다.

template
void func_for_x(T param);
func_for_x(27);

template
void func_for_cx(const T param);
func_for_cx(x);

template
void func_for_rx(const T& param);
func_for_rx(x);

item 1에서 ParamType에 따라서 3 가지 케이스로 나누어 타입 추론을 이해한 것 처럼 auto에도 동일한 규칙이 적용된다.

케이스 1: 타입 구분자(auto)가 포인터나 레퍼런스이고 유니버셜 레퍼런스가 아닐 때.
케이스 2: 타입 구분자가 유니버셜 레퍼런스 일 때.
케이스 3: 타입 구분자가 포인터나 레퍼런스가 아닐 때.

item 1에서 이야기 한 배열이나 함수 인자의 경우 추론에 사용되었던 규칙들 또한 auto에서 동일하게 적용된다.

"더 이상의 자세한 설명은 생략한다."

하지만 이제 한 가지 예외가 등장한다.

C++98 은 여러분에게 int를 선언하는 두 가지 방법을 제공하였다. 그리고 C++11은 두 가지 방법을 추가한다.

int x1 = 27;

int x2 =(27);

int x3 = {27};

int x4{ 27 };

네 가지 다른 선언이 존재하지만 결과는 같다. 값이 27인 int를 선언하는 것이다.

이 때 auto를 사용하면 편리하고 좋지 않을까? 그래서 아래와 같이 선언해보았다.

auto x1 = 27;

auto x2 =(27);

auto x3 = {27};

auto x4{ 27 };


처음 두 개는 정상적으로 동작한다.(기대한대로)
하지만 뒤의 두 개는 int가 아니라 27이라는 값을 가지고 있는 std::initializer_list<int> 라는 타입으로 추론된다.

이것은 auto의 특별한 타입 추론 규칙 때문이다. 만약 auto로 선언된 변수가 중괄호{}로 감싸져있다면, 추론 타입은 std::initializer_list가 된다.
만약 위의 타입으로 추론될 수 없는 경우라면 에러가 난다!

auto x5 = { 1, 2, 5.0 } // 이건 에러다.

이것이 auto와 템플릿 타입 추론의 유일한 차이점이다.

auto x = { 11, 25, 9 } // 이건 정상 동작한다. std::initilizer_list<int> 가 될 것이다.



template<typename t="">

void f(T param);



f({ 11, 25, 9 }); // 이건 에러다. auto와 동일한 선언이지만 템플릿에서는 타입이 추론될 수 없다.



void f(std::initializer_list<t> initList); // 이처럼 직접 선언해주면 정상 동작한다.


C++11에서 의도하지 않게 std::initializer_list 타입의 변수가 선언되는 실수가 종종 발생한다. {}를 쓸 때는 유의하자.

하지만! C++14에서는 또 하나가 추가된다 ㅠㅠ 그지같은..

함수의 반환 타입을 auto로 선언할 수 있는데, 이 때 사용되는 추론규칙은 auto의 그것이 아니라 템플릿의 그것이다.

따라서 아래와 같은 코드는 에러를 내뿜는다.

auto createInitList()
{
  return { 1, 2, 3 }; // 에러.. 추론안됨!
}


auto가 람다의 인자값으로 쓰였을 때에도 같은 에러가 발생할 수 있다.



std::vector<int> v;

...



auto resetV = [&amp;v](const auto&amp; newValue) { v = newValue };



...



resetV({ 1, 2, 3 }); //삐... 자동으로 추론되지 않는다.



----------

핵심요약

1. auto 타입 추론은 template의 타입 추론과 보통 같다고 볼 수 있다. 하지만 {}를 사용해 선언할 때에는 std::initilizer_list 로 추론되므로 유의하자.

2. 함수의 반환값이나 람다의 인자로 auto가 사용된 경우 auto가 아니라 템플릿의 추론규칙을 사용한다.


Effective Modern C++, Item1: 템플릿 타입 추론을 이해하자

c++에서는 템플릿 메소드를 제공한다. 템플릿을 사용하면 컴파일러는 해당 변수의 타입을 추론하는데, 이 추론이 생각과 다르게 동작할 수 있다. 따라서 자세히 어떻게 되는지 알아야 한다.

template<typename T>

void f(ParamType param);



f(expr);


기본적으로 위의 T는 expr의 타입에 따라서 결정되지만 또한 ParamType에도 영향을 받는다. ParamType에 따라 영향을 받는 경우는 3가지로 분류할 수 있다.


케이스 1 : ParamType이 레퍼런스 포인트이고, 유니버셜 레퍼런스가 아닐 때.

가장 간단한 상황으로, ParamType이 레퍼런스 타입이거나 포인터 타입이면서 유니버셜 레퍼런스가 아닐 때 적용된다.

1. 만약 expr의 타입이 레퍼런스라면 레퍼런스 부분을 무시한다.
2. 그리고 expr의 타입과 ParamType을 비교하여 T의 타입을 결정한다.

아래의 예제를 통해 이해해보자.

template<typename T>

void f(T& param);



int x = 27;

const int cx = x;

const int& rx = x;



f(x); // T는 int 이고 param의 타입은 int&가 된다.

f(cx); // T는 const int이고 param의 타입은 const int&가 된다.

f(rx); // T는 const int이고 param의 타입은 const int&가 된다. 레퍼런스는 무시되기 때문.



케이스 2: ParamType이 유니버셜 레퍼런스 일 때

유니버셜 레퍼런스는 T&&와 같이 정의된다.
만약 expr이 lvalue일 때 T와 ParamType은 모두 lvalue 레퍼런스로 추론된다.
만약 expr이 rvalue라면, 케이스1의 규칙을 적용한다.

아래의 예를 통해 이해하자.

template<typename T>

void f(T&& param);



int x = 27;

const int cx = x;

const int& rx = x;



f(x); // x는 lvalue이고 T는 int&가 된다. param의 타입 또한 int&이다.

f(cx); // cx는 lavalue이고 T는 const int&가 된다. param의 타입 또한 const int&이다.

f(rx); // rx는 lavalue이고 T는 const int&가 된다. param의 타입 또한 const int&이다.

f(27); // 27은 rvalue이고 T는 int가 된다. param의 타입은 int&&가 된다.



케이스 3: ParamType이 포인터나 레퍼런스가 아닐 때

ParamType이 포인터나 레퍼런스가 아니라면 값의 복사가 진행된다. param이 새로운 오브젝트로 생성된다는 것은 어떻게 T의 타입이 expr으로 부터 추론되는지 알 수 있게 해준다.

1. 이 전처럼 만약 exprt이 레퍼런스 타입이라면 레퍼런스 부분을 무시한다.
2. expr의 레퍼런스 타입을 무시한 뒤 expr이 const라면 그것 또한 무시한다. 만약 expr이 volatile이라면 이것 또한 무시한다.

따라서 다음 예제와 같은 결과를 확인할 수 있다.

template<typename T>

void f(T param);



int x = 27;

const int cx = x;

const int& rx = x;



f(x); // T와 param 모두 int가 된다.

f(cx); // T와 param 모두 int가 된다. const가 무시되기 때문.

f(rx); // T와 param 모두 int가 된다. 레퍼런스와 const를 무시하기 때문.


여기서 param은 전혀 새로운 오브젝트로 생성되기 때문에 const가 무시되는 것은 당연하게 이해할 수 있다.

아래와 같은 예제를 보자.

template<typename T>

void f(T param);



const char* const ptr = "Fun with pointers";



f(ptr);


위의 ptr의 의미는 ptr이 const 이기 때문에 ptr이 가르키고 있는 주소는 변경될 수 없고, char* 또한 const이기 때문에 char* 배열의 내용 또한 바뀔 수 없다는 것이다.

위와 같이 실행하였을 때 ptr의 값은 복사 될 것이고 ptr의 const는 무시될 것이다. 따라서 param의 타입의 추론은 const char* 가 될 것이다. ptr이 가르키는 대상의 const는 유지되는 것이다.


배열 인자

위의 세 가지 경우 외에도 추가로 알아두면 좋은 것들이 있다. 그 중하나가 배열이 인자로 전달되는 경우이다. 일반적으로 배열은 배열의 첫 번째 값을 가르키는 포인터로 decay 될 수 있다. decay 된다는 뜻은 배열과 배열의 첫번 째 인자를 가르키는 포인터를(비록 같지 않지만) 동일하게 취급하여 컴파일 시 문제가 없게 한다는 것이다.

const char name[] = "J. P. Briggs";



const char * ptrToName = name; // 이처럼 decay 된다고 이해할 수 있다.



하지만 배열이 template에서 by-value로 넘어가면 어떻게 될까?

template<typename T>

void f(T param); // 위에서 언급한 케이스 3에 해당된다.



f(name);



void myFunc(int param[]); // 이와 같은 표현은 적법하지만 배열 선언은 포인터와 같이 취급된다. 따라서 아래와 같다.



void myFunc(int* param);


이러한 문법은 c로부터 온 것이고 위의 경우 두 개의 함수는 동일하다. 이로 인해서 배열과 포인터가 같다고 생각하는 경우가 발생할 수 있다.

위의 경우 name이 포인터로 decay 되어 넘어가기 때문에 param의 타입은 const char*가 될 것이다.

하지만 만약 아래와 같이 받으면 어떻게 될까?

template<typename T>

void f(T& param);



f(name);


위의 경우 name이라는 배열에 대한 레퍼런스가 생성되어 param의 타입이 된다. 즉, const char[13] 그대로 name의 타입을 인식하고, param의 타입은 const char (&)[13] 이 된다.


함수 인자

c++ 에서 포인터로 decay되는 것은 배열만이 아니라 함수도 있다. 그리고 우리가 array에 대해서 이야기했던 모든 규칙들은 함수에도 적용이 된다.

void someFunc(int, double);



template<typename T>

void f1(T param);



template<typename T>

void f2(T& param);



f1(someFunc); // someFunc가 포인터로 decay 되어 param의 타입은 void (*)(int, double)이 된다.



f2(someFunc); // someFunc에 대한 레퍼런스로 받아 param의 타입은 void (&)(int, double)이 된다.



-------

핵심 요약

1. 템플릿 타입 추론 도중 레퍼런스인 인자들의 레퍼런스 성질은 무시된다.

2. 유니버셜 레퍼런스 인자를 추론할 때 lvalue는 특별 취급을 받는다.

3. by-value 인자를 추론할 때 const와 volatile은 그 성질을 잃는다.

4. 템플릿 타입 추론 도중 배열이나 함수인 인자는 포인터로 decay 된다. T가 레퍼런스라면 해당 값에 대한 레퍼런스로 받는다.


2016년 3월 15일 화요일

정규표현식 문법 기초

본 포스트는 다음의 pdf 파일을 참고(번역)해서 작성했습니다.
http://www.lunametrics.com/regex-book/Regular-Expressions-Google-Analytics.pdf


백슬래시(backslash) \

정규표현식을 배울 때 가장 먼저 시작하면 좋은 것은 백슬래시(\) 입니다. 백슬래시는 정규표현식과 일반 텍스트를 구분해주는 역할을 합니다. 백슬래시 다음에 오는 문자는 정규표현식이 아닌 일반 캐릭터로 인식을 합니다.

예를 들면 /folder?pid=123 이라는 글자를 가져오려고 할 때 '?'는 어떻게 찾아야 할까요? ?는 정규표현식에서 이미 예약하여 사용하는 표현입니다. 이럴 때
/folder\?pid=123
처럼 사용합니다. 여기서 백슬래시는 ?앞에 사용되어서 ?가 정규표현식의 일부가 아니라 일반 캐릭터라는 것을 알려줍니다.


파이프(pipe) |

파이프는 or를 의미합니다.
Coke|Pepsi
처럼 사용할 수 있습니다.


물음표(question mark) ?

물음표는 마지막 캐릭터는 optional이다. 라는 의미입니다.
Robb?in 을 검색하면 Robin과 Robbin 둘 다 찾아줍니다. 마지막 b는 optional 이니까요.


괄호(parentheses) ()

정규표현식에서 괄호는 수학에서의 괄호와 똑같이 사용됩니다.
/folder(one|two)/thanks
는 /folderone/thanks와 /foldertwo/thanks 모두 찾아줍니다.
위에서 언급된 ?와 함께 사용하여 다음과 같은 정규표현식을 작성할 수 있습니다.
/thanks(alot)?
만약 /thanks, /thankyou, thanksalot 세 개의 폴더가 있다면 thankyou는 검색되지 않을 것 입니다.


큰괄호(square brackets) [] 와 다시(dashes) -

큰괄호와 다시는 뗄래야 뗄 수 없는 관계입니다. 먼저 큰 괄호만 사용하면 간단한 리스트를 만드는 의미가 있습니다.
p[aiu]n
이라는 정규표현식을 사용하면 pan, pin, pun은 검색이 되지만 pain은 검색되지 않습니다.

또한 다시를 사용하여 조금 더 긴 리스트를 만들 수 있습니다.
[a-z]는 모든 영어 소문자 알파벳을 의미합니다.
[A-Z]는 모든 영어 대문자 알파벳을 의미합니다.
[a-zA-Z0-9]는 모든 영어 소문자, 대문자 알파벳과 0~9 사이의 숫자를 의미합니다.

활용 예를 하나 더 들면, sneakers401, sneakers408, sneakers101 등의 문자들이 있을 때 이 중 sneakers401과 sneakers408만 뽑고 싶다면 다음과 같이 작성하면 된다.
sneakers40[0-8]


중괄호(braces) {}

중괄호는 반복 횟수를 나타냅니다. 중괄호 안에는 {x, y} 처럼 두 개의 숫자나 {z} 처럼 한 개의 숫자가 들어올 수 있습니다. 두 개의 숫자가 있을 때에는 마지막 캐릭터를 최소 x번 최대 y번 반복하는걸 의미합니다. 숫자가 한개만 있을 때에는 정확히 z번 반복하는 것을 의미합니다.

예를 들어 123.145.167.0 과 123.145.167.99 두 개의 아이피를 찾고 싶다면 다음과 같이 작성할 수 있습니다.
123\.145\.167\.[0-9]{1,2}


점(dot) .
점은 단 하나의 캐릭터를 매치합니다. 단 하나의 캐릭터란 공백을 포함한 모든 종류의 캐릭터를 의미합니다. 점 자체로는 큰 쓸모가 없지만 다른 정규식과 결합하여 강력한 힘을 발휘합니다.
.ite
는 site, lite, bite, kite 를 모두 찾아줍니다. $ite %ite 같은 것들도 물론이구요. 하지만 ite를 찾지는 않습니다. ite앞에 캐릭터가 존재하지 않기 때문입니다.
?? 위에서는 공백도 찾아준다고 했는데.. 확인해봐야 할 듯..


더하기(plus sign) +
더하기 표시는 하나 혹은 그 이상의 캐릭터가 반복되는 것을 의미합니다.
aa+rgh
라는 정규식은 aargh, aaaaargh 모두 찾아줍니다. 단 argh는 찾지 않습니다. +는 하나 혹은 그 이상을 의미하니까요.


곱하기(star) *
곱하기 표시는 제로 혹은 그 이상의 아이템을 의미합니다. 더하기 표시와 거의 흡사하지만 캐릭터가 없어도 찾아준다는 부분이 다릅니다.
aa*rgh
는 aa+rgh가 찾는 모든 것을 찾아주고 추가로 argh도 찾습니다. *는 0개도 포함하니까요.


점 별(dot star) .*

모든것을 찾아라, 라는 의미의 정규식이 존재합니다. 
/folderone/.*index\.php
위의 예제에서 정규식은 folderone/ 로 시작하고 index.php 로 끝나는 모든 값들을 찾습니다. 


꺽쇄(caret) ^

정규식에서 꺽쇄를 사용하면 정규식에서 지정한대로 정확하게 시작하는 부분만 찾는것을 의미합니다.
^/subfolder2/index\.html
이라는 정규식을 작성하였다면, 문자열은 반드시 /subfolder 로 시작하여야 합니다. 만약 문자열이 /folder1/subfolder2/index.html 이라면, 절대 찾아지지 않을 것입니다.


달러 표시(dollar sign) $

달러 표시의 의미는 문자열에서 내 정규식의 $표시 뒤에 추가적인 문자열이 존재한다면 찾지 않겠다는 뜻입니다. 위에서 언급한 꺽쇄와 같이 사용한다면 다음과 같은 정규식을 작성할 수 있습니다.
^/$
이는 /mysite.com/folder/ 처럼 /로 시작하고 /로 끝나는 문자열만 매치하겠다는 의미입니다.



2016년 3월 9일 수요일

Effective Modern C++, Item4: 추론 타입을 확인하는 방법

타입 추론을 확인하기 위해 사용되는 툴은 개발 프로세스에 따라 달라진다. 여기서는 다음과 같은 세 가지 상태에 대해서 알아본다: 코드 에디트, 컴파일 단계, 런타임.

IDE 에디터

const int theAnswer = 10;
auto x = theAnswer;
auto y = &theAnswer;
IDE 에디터(Visual Studio 등)는 변수에 마우스를 올리면 x는 int로, y는 const int*로 추론되었다는 것을 보여줄 것이다.

이렇게 작동하게 하기 위해서 IDE는 내부적으로 C++ 컴파일러를 동작시킨다. 만약 이 컴파일러가 타입을 추론하기에 부족한 상태라면 추론된 타입을 보여줄 수 없다. 따라서, int 같은 단순한 형태라면 문제가 없지만 더 호환성 있는 타입이 사용된다면 IDE에 표시되는 정보는 유용하지 않을 수 있다.

컴파일러 진단


컴파일러를 사용하여 타입을 추론하는 효과적인 방법은 추론된 타입을 컴파일 에러가 나도록 사용하는 것이다. 이렇게 하면 에러 메세지를 통해 추론된 변수의 타입을 확인할 수 있다.

예를 들면, 먼저 아래와 같은 형태의 정의되지 않은 템플릿을 만들 수 있다.
template<typename T>
class TD;
그리고 이러한 템플릿을 인스턴스화 하는 것은 에러 메시지를 발생시킬 것이다. 위의 예제에서의 x와 y의 타입을 알고 싶다면 아래와 같이 사용할 수 있다.
TD<decltype(x)> xType;
TD<decltype(y)> yType;

런타임 결과

실행 화면에서 표시될 수 있는 텍스트의 형태로 타입을 출력하기 위해서 아래와 같은 방법을 사용할 수 있다.
std::cout << typeid(x).name() << '\n';
std:;cout << typeid(y).name() << '\n';
컴파일러마다 표시되는 문자가 다를 수 는 있지만 마이크로소프트의 컴파일러는 정확한 결과를 보여준다. 하지만 그렇다고 문제가 해결된 것은 아니다. 조금 더 복잡한 경우를 살펴보자.
template<typename T>
void f(const T& param);
std::vector<Widget> createVec();
const auto vw = createVec();
if(!vm.empty()) {
f(&vm[0]);
...
}
위 코드에 typeid를 확인하기 위해서 f 함수를 아래와 같이 정의한다.
template
void f(const T& param)
{
  using std::cout;
  cout << "T = " << typeid(T).name() << '\n';
  cout << "param = " << typeid(param).name() << '\n';
  ...
컴파일 결과는 마이크로소프트 컴파일러의 경우 둘 다 class Widget const * 로 표시된다.
하지만 소스에서 param 의 타입은 const T&로, T와는 다르다. 이처럼 std::type_info::name 은 항상 신뢰할 수 는 없다.

그 이유는 std::type_info::name의 특성 상 템플릿 함수를 by-value 파라미터로 받는다는데 있다. 만약 타입이 레퍼런스이면, 그 레퍼런스의 특징이 삭제(무시)되고, 삭제된 레퍼런스가 const라면 그 또한 삭제된다. 따라서 const Widget * const & 가 const Widget * 이 된 것이다.

이러한 현상은 IDE에서도 동일하게 일어난다. 해결방법 중 하나는 Boost TypeIndex 라이브러리를 사용하는 것이다. Boost를 사용하면 다음과 같이 표현할 수 있다.
#include 

template
void f(const T& param)
{
  using std::cout;
  using boost::typeindex::type_id_with_cvr;

  //show T
  cout << "T = " << type_id_with_cvr().pretty_name() << '\n';

  //show param's type
  cout << "param = " << type_id_with_cvr().pretty_name() << '\n';
  ...
}
이것이 작동하는 방법은 boost::typeindex::type_id_with_cvr 이 우리가 알고자 하는 변수를 인자로 받아서 const와 volatile, 혹은 & 성질을 지우지 않고 이용하는 것이다. .pretty_name()은 인자의 속성을 std::string으로 반환해준다.

이 같은 방법을 사용하면 T의 타입은 Widget const* 로, param의 타입은 Widget const* const&로 정확하게 알 수 있다.