제 18 장: 표준 템플릿 라이브러리

표준 템플릿 라이브러리(Standard Template Library(STL))는 범용 목적의 라이브러리이다. 컨테이너와 총칭 알고리즘 그리고 반복자와 함수객체 또 배당자와 어댑터 마지막으로 데이터 구조로 구성된다. 알고리즘이 사용하는 데이터 구조는 실제로 어떠한 데이터 유형과도 사용이 가능하다는 점에서 추상적이다.

알고리즘이 추상 데이터 유형을 처리할 수 있는 까닭은 템플릿(template)에 기반하고 있기 때문이다. 이 장에서는 템플릿의 생성은 다루지 않는다 (생성에 관해서는 제 21장 참고). 그보다는 알고리즘의 사용법에 초점이 있다.

표준 템플릿 라이브리에서 사용되는 여러 요소들을 이미 다루어 보았다. 제 12장에 컨테이너를 다루었고 11.10절에 함수객체를 소개했다. 또한 반복자도 여러 곳에 언급한 바가 있다.

STL의 주요 구성요소를 이 장과 다음 장에 다룬다. 다음 절부터 STL의 반복자, 어댑터, 스마트 포인터, 다중 쓰레드 그리고 기타 특징들을 다룬다. 총칭 알고리즘은 다음 장에 다룬다 (제 19장).

배당자(Allocator)는 STL에서 메모리 배당을 관리한다. 기본 배당자 만으로도 대부분의 어플리케이션을 만족시키므로 더 깊게 다루지 않는다.

STL의 모든 요소들은 표준 이름공간에 정의되어 있다. 그러므로 using namespace std 또는 그 비슷한 지시어가 필요하다. 물론 이름공간을 명시적으로 지정하고 싶지 않다면 상관이 없다. 헤더 파일에 std 이름공간을 명시적으로 지정해야 한다 (7.11.1항).

이 장은 빈 옆꺽쇠 표기법을 자주 사용한다. 코드에서 유형이름은 옆꺽쇠 사이에 놓아야 한다. 이 책에서는 plus<> 표기법을 사용하더라도 실전 코드에서는 plus<string> 형태를 만날 것이다.

18.1: 미리 정의된 함수객체

미리 정의된 함수객체를 사용하려면 먼저 <functional> 헤더를 포함해야 한다.

함수객체는 총칭 알고리즘에서 중요한 역할을 한다. 예를 들어 sort 총칭 알고리즘은 두 개의 반복자를 기대한다. 하나는 정렬될 객체의 범위이고 또 하나는 비교 연산자를 호출하는 함수객체이다. 다음 상황을 잠시 살펴보자. 문자열이 벡터에 저장되어 있다고 가정한다. 그 벡터를 내림차순으로 정렬하고 싶다면 다음과 같이 stringVec 벡터를 간단하게 정렬할 수 있다.

        sort(stringVec.begin(), stringVec.end(), greater<string>());
마지막 인자는 생성자로 인식한다. greater<> 클래스 템플릿을 구체화하여 문자열(string)에 적용한다. 이 객체를 sort 총칭 알고리즘에서 함수객체로 호출한다. 이 총칭 알고리즘은 그 함수객체의 operator() 멤버를 호출해서 두 개의 string 객체를 비교한다. 함수객체의 operator()는 이어서 string 데이터 유형의 operator>를 호출한다. 마침내 sort가 돌아오면 벡터의 첫 번째 원소에 모든 원소 중 가장 큰 값을 가지는 string이 들어 있게 된다.

함수객체의 operator() 자체는 이 시점에서 보이지 않는다. `greater<string>()' 인자에 있는 반괄호와 호출하는 operator()의 반괄호를 서로 혼동하지 마라. operator()가 실제로 sort 안에 사용될 때, `크기' 비교를 할 문자열 두 개를 인자로 받는다. greater<string>::operator() 연산자는 인라인으로 정의되어 있으므로 호출 자체는 실제로 위의 sort 호출에 존재하지 않는다. 대신에 sortgreater<string>::operator()를 통하여 string::operator>를 호출한다.

생성자가 총칭 알고리즘에 인자로 건네진다는 사실을 알았으므로 스스로 함수객체를 설계할 수 있다. 대소문자를 구별하지 않고 벡터를 정렬하고 싶다고 해 보자. 어떻게 처리할 것인가? 먼저, 기본(오름 차순)의 string::operator<는 적절하지 않음을 알 수 있다. 대소문자를 구별해 비교하기 때문이다. 그래서 따로 CaseInsensitive 클래스를 공급하자. 두 개의 문자열을 대소문자를 구별하지 않고 비교한다. 다음 프로그램은 POSIX strcasecmp 함수를 사용하여 임무를 수행한다. 대소문자를 구별하지 않고 명령줄 인자를 오름차순으로 정렬한다.

    #include <iostream>
    #include <string>
    #include <cstring>
    #include <algorithm>
    using namespace std;

    class CaseInsensitive
    {
        public:
            bool operator()(string const &left, string const &right) const
            {
                return strcasecmp(left.c_str(), right.c_str()) < 0;
            }
    };
    int main(int argc, char **argv)
    {
        sort(argv, argv + argc, CaseInsensitive());
        for (int idx = 0; idx < argc; ++idx)
            cout << argv[idx] << " ";
        cout << '\n';
    }

class CaseInsensitive의 기본 생성자를 사용하여 sort에 마지막 인자로 제공한다. 그래서 CaseInsensitive::operator()만 정의하면 된다. string 인자로 호출될 것임을 알고 있으므로 두 개의 string 인자를 기대하도록 정의한다. 이 인자들은 strcasecmp를 호출할 때 사용된다. 게다가 operator() 연산자는 인라인으로 정의되어 있으므로 sort 함수가 호출될 때의 부담을 덜어준다. sort 함수는 문자열(string)을 다양하게 조합하여 함수객체를 호출한다. 컴파일러가 인라인 요청을 받아들이면 실제로 두 개의 추가 함수 호출을 건너뛰고 strcasecmp를 호출한다.

비교 함수객체는 미리 정의되어 있는 경우가 많다. 미리 정의된 함수객체는 빈번히 사용되는 많은 연산에 사용할 수 있다. 다음 절에 그 사용법을 예제와 함께 소개한다. 말미에 함수 어댑터(function adaptor)를 소개한다.

미리 정의된 함수객체는 총칭 알고리즘에 아주 많이 사용된다. 산술 연산과 관계 연산 그리고 논리 연산을 위해 존재한다. 미리 정의된 함수객체를 발전시켜 24.3절에서 비트 연산을 수행시켜 보겠다.

18.1.1: 산술 함수객체

산술 함수객체는 덧셈과 뺄셈 그리고 곱셈과 나눗셈 그리고 나머지와 부인의 표준 산술 연산을 지원한다. 이런 함수객체는 상응하는 연산자를 실체화된 자신의 데이터 유형에 요청한다. 예를 들어 덧셈에 대하여 plus<Type> 함수객체를 사용할 수 있다. Typesize_t로 바꾸면 size_t 값의 덧셈 연산자가 사용된다. Typestring으로 바꾸면 문자열에 대한 덧셈 연산자가 사용된다. 예를 들어:
    #include <iostream>
    #include <string>
    #include <functional>
    using namespace std;

    int main(int argc, char **argv)
    {
        plus<size_t> uAdd;              // size_ts를 더하는 함수객체

        cout << "3 + 5 = " << uAdd(3, 5) << '\n';

        plus<string> sAdd;              // 문자열을 더하는 함수객체

        cout << "argv[0] + argv[1] = " << sAdd(argv[0], argv[1]) << '\n';
    }
    /*
        출력: a.out going

        3 + 5 = 8
        argv[0] + argv[1] = a.outgoing
    */

이것이 왜 유용한가? 함수객체는 (미리 정의된 데이터 유형말고도) 온갖 데이터 유형에 사용할 수 있음을 눈여겨보라. 함수객체가 호출하는 연산자를 데이터 유형이 지원하기만 하면 된다.

왼쪽 피연산자는 언제나 같은 변수이고 오른쪽 인자는 순서대로 한 배열의 모든 원소를 사용해야 할 경우가 있다. 배열 안의 모든 원소들의 합을 계산하든가 또는 텍스트 배열 안의 모든 문자열을 결합하고 싶다고 하자. 이런 상황에 함수객체를 편리하게 사용할 수 있다.

앞서 언급했듯이 함수객체는 총칭 알고리즘의 맥락에 중요하게 사용된다. 그래서 예제를 하나 더 살펴보자.

accumulate 총칭 알고리즘은 반복자 범위로 지정된 원소들을 모두 방문한다. 그리고 공통 원소와 범위 안의 각 원소들에 대하여 이항 연산을 수행한다. 반복자 범위로 지정된 모든 원소들을 방문한 후에 축적된 결과를 돌려준다. 이 알고리즘의 사용법은 아주 쉽다. 다음 프로그램은 모든 명령 줄 인자를 합산하여 최종 문자열을 인쇄한다.

    #include <iostream>
    #include <string>
    #include <functional>
    #include <numeric>
    using namespace std;

    int main(int argc, char **argv)
    {
        string result =
                accumulate(argv, argv + argc, string(), plus<string>());

        cout << "All concatenated arguments: " << result << '\n';
    }

앞쪽의 두 인자는 방문할 원소의 범위 (반복자)를 정의하고 세 번째 인자는 string이다. 이 이름없는 문자열 객체는 최초 값을 제공한다. 다음과 같이 사용해도 되는데

    string("All concatenated arguments: ")
이 경우 cout 서술문은 그냥 cout << result << '\n'이면 된다. 문자열-덧셈 연산이 사용되었다. plus<string>으로부터 호출된다. 결합된 문자열이 최종 반환된다.

Time 클래스를 정의해 보자. operator+를 중복정의한다. 미리 정의된 plus 객체를 적용할 수도 있다. 새로 정의된 데이터 유형에 맞추어 재단되었으므로 시간을 추가하려면:

    #include <iostream>
    #include <string>
    #include <vector>
    #include <functional>
    #include <numeric>
    using namespace std;

    class Time
    {
        friend ostream &operator<<(ostream &str, Time const &time);
        size_t d_days;
        size_t d_hours;
        size_t d_minutes;
        size_t d_seconds;
        public:
            Time(size_t hours, size_t minutes, size_t seconds);
            Time &operator+=(Time const &rValue);
    };
    Time &&operator+(Time const &lValue, Time const &rValue)
    {
        Time ret(lValue);
        return std::move(ret += rValue);
    }
    Time::Time(size_t hours, size_t minutes, size_t seconds)
    :
        d_days(0),
        d_hours(hours),
        d_minutes(minutes),
        d_seconds(seconds)
    {}
    Time &Time::operator+=(Time const &rValue)
    {
        d_seconds   += rValue.d_seconds;
        d_minutes   += rValue.d_minutes   + d_seconds / 60;
        d_hours     += rValue.d_hours     + d_minutes / 60;
        d_days      += rValue.d_days      + d_hours   / 24;
        d_seconds   %= 60;
        d_minutes   %= 60;
        d_hours     %= 24;
        return *this;
    }
    ostream &operator<<(ostream &str, Time const &time)
    {
        return cout << time.d_days << " days, " << time.d_hours <<
                                                    " hours, " <<
                        time.d_minutes << " minutes and " <<
                        time.d_seconds << " seconds.";
    }
    int main(int argc, char **argv)
    {
        vector<Time> tvector;

        tvector.push_back(Time( 1, 10, 20));
        tvector.push_back(Time(10, 30, 40));
        tvector.push_back(Time(20, 50,  0));
        tvector.push_back(Time(30, 20, 30));

        cout <<
            accumulate
            (
                tvector.begin(), tvector.end(), Time(0, 0, 0), plus<Time>()
            ) <<
            '\n';
    }
    //  출력: 2 days, 14 hours, 51 minutes and 30 seconds.

위 프로그램의 설계는 보이는 그대로이다. Time은 생성자를 정의하고 삽입 연산자를 정의하며 그리고 자신만의 operator+ 연산자를 정의해 두 개의 시간 객체를 더한다. main 함수에서 네 개의 Time 객체가 vector<Time> 객체 안에 저장된다. 다음, 축적된 시간을 accumulate로 계산한다. Time 객체를 돌려주면 이 객체를 cout에 삽입한다.

첫 예제는 이름 붙은 함수객체의 사용법을 보여주었지만 마지막 두 예제는 (accumulate) 함수에 건네어진 이름 없는 객체의 사용법을 보여주었다.

STL은 다음의 산술 함수객체를 지원한다. 산술 함수객체의 operator() 함수 호출 연산자는 건네받은 객체에 부합하는 산술 연산자를 호출하고 그 반환 값을 돌려준다. 실제로 호출되는 산술 연산자를 아래에 기술한다.

다음 예제는 transform 총칭 알고리즘을 사용하여 배열 안에 있는 원소의 부호를 모두 뒤집는다. transform 알고리즘이 기대하는 두 개의 반복자는 변형할 객체의 범위를 정의한다. 목표 범위의 시작이 정의된 반복자와 (첫 인자와 같은 반복자일 수 있다) 그리고 지시된 데이터 유형에 대하여 단항 연산을 정의한 함수객체를 기대한다.
    #include <iostream>
    #include <string>
    #include <functional>
    #include <algorithm>
    using namespace std;

    int main(int argc, char **argv)
    {
        int iArr[] = { 1, -2, 3, -4, 5, -6 };

        transform(iArr, iArr + 6, iArr, negate<int>());

        for (int idx = 0; idx < 6; ++idx)
            cout << iArr[idx] << ", ";
        cout << '\n';
    }
    // 출력:  -1, 2, -3, 4, -5, 6,

18.1.2: 관계형 함수객체

관계 연산자는 관계형 함수객체에 의하여 호출된다. 다음의 ==, !=, >, >=, < 그리고 <= 표준 관계 연산자를 모두 지원한다.

STL은 다음의 관계형 함수객체 집합을 지원한다. 관계형 함수객체의 operator() 함수 호출 연산자는 건네받은 객체에 부합하는 관계 연산자를 호출한 다음에 그 반환 값을 돌려준다. 실제로 호출되는 관계 연산자를 아래에 기술한다.

sort와 조합하여 관계형 함수객체를 사용하는 예는 다음과 같다.
    #include <iostream>
    #include <string>
    #include <functional>
    #include <algorithm>
    using namespace std;

    int main(int argc, char **argv)
    {
        sort(argv, argv + argc, greater_equal<string>());

        for (int idx = 0; idx < argc; ++idx)
            cout << argv[idx] << " ";
        cout << '\n';

        sort(argv, argv + argc, less<string>());

        for (int idx = 0; idx < argc; ++idx)
            cout << argv[idx] << " ";
        cout << '\n';
    }

예제는 문자열을 알파벳 순서로 저장하고 뒤집는 방법을 보여준다. greater_equal<string>을 건네면 문자열은 내림차순으로 저장된다 (첫 단어는 '가장 큰' 단어가 된다). less<string>를 건네면 문자열은 오름차순으로 저장된다 (첫 단어는 '가장 작은' 단어가 된다).

argvchar * 값이 들어 있고 관계 함수객체는 string을 기대한다는 것을 주의하라. char const *으로부터 string으로 조용하게 승격된다.

18.1.3: 논리 함수객체

논리 연산자는 논리 함수객체에 의하여 호출된다. 표준으로 andor 그리고 not 논리 연산자를 지원한다.

STL은 다음의 논리 함수객체 집합을 지원한다. 이 함수객체의 operator() 함수 호출 연산자는 건네받은 객체에 부합하는 논리 연산자를 호출하고 그 반환 값을 돌려준다. 실제로 호출되는 논리 연산자는 아래에 기술한다.

operator!를 사용하는 예를 다음 프로그램에 보여준다. transform 알고리즘을 사용하여 논리 값을 배열로 변환한다.
    #include <iostream>
    #include <string>
    #include <functional>
    #include <algorithm>
    using namespace std;

    int main(int argc, char **argv)
    {
        bool bArr[] = {true, true, true, false, false, false};
        size_t const bArrSize = sizeof(bArr) / sizeof(bool);

        for (size_t idx = 0; idx < bArrSize; ++idx)
            cout << bArr[idx] << " ";
        cout << '\n';

        transform(bArr, bArr + bArrSize, bArr, logical_not<bool>());

        for (size_t idx = 0; idx < bArrSize; ++idx)
            cout << bArr[idx] << " ";
        cout << '\n';
    }
    /*
      출력:

        1 1 1 0 0 0
        0 0 0 1 1 1
    */

18.1.4: 함수 어댑터

함수 어댑터는 기존 함수객체의 작동을 변조한다. STL은 세 종류의 함수 어댑터를 제공한다. 바인더와 부인자 그리고 멤버 함수 포장자를 제공한다. 바인더와 부인자는 다음 두 하위 목에 기술한다. 멤버 함수 어댑터는 현재 장보다 다루기에 더 자연스러운 다음 장 19.2절에 다룬다.

18.1.4.1: `bind' 함수 템플릿

바인더는 이항 함수객체를 단항 함수객체로 변환하는 함수 어댑터이다. 이항 함수객체의 매개변수 하나를 상수 값에 묶는다. 예를 들어 minus<int> 함수객체의 첫 매개변수가 100에 묶여 있다면 결과 값은 언제나 100에서 두 번째 인자의 값을 뺀 것이다.

원래는 bind1stbind2nd 두 개의 바인더 어댑터가 정의되어 있었다. 각각 이항 함수의 첫 번째 인자와 두 번째 인자를 나타낸다. 그렇지만 다음 C++17 표준에서 bind1stbind2nd는 제거될 가능성이 높다. 좀 더 일반적인 bind 바인더로 교체할 수 있기 때문이다. bind 함수 자체가 비추천될 가능성이 높다. 총칭적인 람다 함수로 어렵지 않게 교체할 수 있기 때문이다 (18.7절).

bind1stbind2nd를 여전히 사용할 수 있기 때문에 bind2nd를 중심으로 짧은 예제에 그 사용법을 보여준다. 좀 더 우아하게 bind를 사용하는 방법은 다음 예제에 보여준다. 기존의 코드는 bind나 람다 함수를 사용하도록 수정해야 한다.

bind를 또는 std::placeholders 이름공간을 사용하기 전에 <functional> 헤더를 포함해야 한다.

다음은 bind2nd의 사용법을 보여준다. vs 문자열 벡터에서 target 문자열과 같은 원소의 갯수를 센다 (필요한 데이터와 using namespace std가 이미 지정되어 있다고 간주한다):

    count_if(vs.begin(), vs.end(), bind2nd(equal_to<string>(), target));
이 예제에서 문자열에 대하여 equal_to 함수객체가 구체화된다. target을 두 번째 인자로 받고 vs안의 문자열마다 하나씩 연속적으로 첫 인자에 건넨다. 이 특별한 예제에서 동등성이 결정되는 곳에 bind1st를 사용할 수도 있었다.

bind 어댑터는 함수를 첫 인자로 기대한다. 그 다음에 함수가 필요한 인자를 얼마든지 건네도 된다. bind를 사용할 때 갯수 없이 인자를 지정하고 있지만 C 언어에서 정의하는 방식의 가변 함수가 아니다. bind가변 함수 템플릿이다 (22.5절).

기본값으로 bind는 첫 인자로 지정된 함수를 돌려준다. 그리고 건네어진 나머지 인자들을 받는다. 그러면 bind가 돌려준 함수를 호출할 수 있다. bind가 호출된 방식에 따라, 반환된 함수를 호출하면 인자를 요구할 수도 안할 수도 있다.

다음은 한 예이다.

    int sub(int lhs, int rhs);      // lhs - rhs를 돌려준다.
    bind(sub, 3, 4);                // 함수객체를 돌려준다.
                                    // operator()는 sub(3, 4)을 돌려준다.
bind의 반환 값은 함수객체이므로 호출할 수 있다.
    bind(sub, 3, 4)();
그러나 bind의 반환 값은 변수에 할당되는 경우가 더 많다. 그러면 반환된 객체를 다음과 같이 나타낸다.
    auto functor = bind(sub, 3, 4); // 함수객체를 위한 변수
    cout << functor() << '\n';      // 함수객체를 호출,  -1을 돌려줌.

bind를 사용할 때 인자를 지정하는 대신에 위치 보유자를 지정할 수 있다 (4.1.3.1목). 반환된 객체를 호출할 때 인자 값을 명시적으로 지정해야 한다. 다음은 몇 가지 예이다.

    using namespace placeholders;
    auto ftor1 = bind(sub, _1, 4);  // 첫 번째 인자를 지정해야 한다.
    ftor1(10);                      // 10 - 4 = 6을 반환

    auto ftor2 = bind(sub, 5, _1);  // 두 번째 인자를 지정해야 한다.
    ftor2(10);                      // 5 - 10 = -5를 반환
        
    auto ftor3 = bind(sub, _1, _2); // 두 인자를 모두 지정해야 한다.
    ftor3(10, 2);                   // 10 - 2 = 8을 반환 
        
    auto ftor3 = bind(sub, _2, _1); // 두 인자를 모두 지정해야 한다.
    ftor3(10, 2);                   // 그러나 순서는 거꾸로이다.
                                    // 2 - 10 = -8을 반환
대안으로 첫 인자는 멤버 함수의 주소가 될 수 있다. 그 경우에 첫 인자는 멤버 함수가 호출될 객체를 지정해야 한다. 반면에 나머지 인자들은 (있다면) 멤버 함수에 건네어질 인자들을 지정한다. 다음은 몇 가지 예이다.
struct Object                   // 이 객체는 뺄셈 연산의
{                               // lhss가 들어 있다.
    int d_lhs;

    Object(int lhs)
    :
        d_lhs(lhs)
    {}
    int sub(int rhs)            // sub는 d_lhs를 변경한다.
    {
        return d_lhs -= rhs;
    }
};

int main()
{
    using namespace placeholders;

    Object obj(5);

    auto ftor = bind(&Object::sub, obj, 12);
    cout << ftor() << '\n';     // -7을 보여준다.
    cout << obj.d_x << '\n';    // obj는 변경되지 않는다. bind가 사본을 사용하기 때문이다.

    auto ftor = bind(&Object::sub, ref(obj), 12);
    cout << ftor() << '\n';     // -7을 보여준다.
    cout << obj.d_x << '\n';    // obj가 변경됨, cout은 -7을 보여줌
}
두 번째 bind 호출에 ref를 사용한 것에 주목하라: 여기에서 obj는 참조로 건네진다. for2 함수객체에 사본이 아니라 obj 자체를 전달한다. 이것은 완벽한 전달(perfect forwarding)이라는 편의기능을 사용하여 실현된다. 이에 관해서는 22.5.2항에 더 자세하게 논의한다.

함수객체가 호출한 함수의 반환 유형이 문맥에 부합하지 않으면 (예를 들어 함수객체가 표현식 안에서 호출되었는데 반환 값이 size_t라면) 적절한 유형으로 변환할 수 있다. 그 경우 bind 다음 옆꺽쇠 사이에 반환 유형을 요청할 수 있다. 예를 들어,

    auto ftor = bind<size_t>(sub, _1, 4);   // ftor의 반환 유형은 size_t이다.
    size_t target = 5;
    if (target < ftor(3))           // -1은 거대한 양수 값이 된다.
        cout << "smaller\n";        // 그래서 'smaller'를 보여준다.
마지막으로 bind2nd를 사용한 이전 예제는 bind를 사용하여 다음과 같이 다시 작성할 수 있다.
    using namespace placeholders;
    count_if(vs.begin(), vs.end(), bind(equal_to<string>(), _1, target));
여기에서 bind는 인자를 하나 기대하는 함수객체를 돌려준다 (_1로 표현됨). 그리고 count_ifvs에 있는 문자열들을 순서대로 bind가 돌려주는 함수객체에 건넨다. 두 번째 인자는 (target) 함수객체의 구현 안에 내장된다. 거기에서 equal_to<string>() 함수객체에 두 번째 인자로 건네진다.

18.1.4.2: 부인자

부인자(Negator) 는 진위 함수가 돌려주는 논리값을 뒤집는 함수 어댑터이다. 전통적으로 bind1stbind2nd에 짝을 맞추어 두 개의 부인자 함수 어댑터가 미리 정의되어 있었다. not1 부인자는 단항 진위 함수와 함께 사용되고 not2 부인자는 이항 진위 함수와 함께 사용된다. 어떤 상황에서는 여전히 bind 함수 템플릿과 조합하여 사용하면 유용하다. 그러나 bind1stbind2ndC++17에서 제거될 것이므로 not1not2에 대해서도 대책을 마련해야 한다 (https://isocpp.org/files/papers/n4076.html 참고).

not1not2는 여전히 C++ 표준이므로 다음에 그 사용법을 간략하게 보여준다. 어떻게 미래의 not_fn이 설계될 것인가 그리고 그 사용법을 제시하면서 그 대안을 22.5.5항에 구현해 본다.

다음은 not1not2의 사용법을 보여주는 예제들이다. 문자열 벡터 (vs)에서 어떤 참조 문자열 (target) 보다 먼저 알파벳 순서로 먼저 오는 원소의 갯수를 세려면 다음 대안 중 하나를 사용할 수 있다.

18.2: 반복자

STL에 정의된 여러 어댑터를 사용하면 객체들을 반복자로 건넬 수 있다. 이 절은 지금까지 보여준 개념적인 반복자 유형말고도 이런 어댑터들을 소개한다. 사용하기 전에 먼저 <iterator> 헤더를 포함해야 한다.

표준 반복자도 비추천 후보이지만 곧바로 표준 라이브러리로부터 제거된다는 뜻은 아니다. 자신만의 수레 바퀴를 발명하는 짓은 별로 효율적인 전략은 아니다. 대신에 공식적으로 교체될 때까지는 std::iterator 클래스를 사용하자.

반복자는 포인터처럼 행위하는 객체이다. 반복자는 일반적으로 다음과 같은 특징이 있다.

일반적으로 STL 컨테이너는 반복자를 제공하는 멤버를 정의한다. 즉, 자신만의 iterator 유형을 정의한다. 이 멤버들을 정방향 반복자에 대하여 beginend라고 부른다. 역방향 반복자에 대하여 rbeginrend이라고 부른다.

표준 관례는 반복자 범위에 왼쪽이 포함된다. [left, right) 표기법은 left가 첫 원소를 가리키는 반복자이고, 반면에 right는 마지막 원소 바로 다음을 가리키는 반복자라는 뜻이다. left == right이면 반복자 범위는 비어 있다.

다음 예제는 문자열 벡터의 모든 원소들을 어떻게 cout에 삽입하는지 보여준다. 반복자 [begin(), end()) 범위와 [rbegin(), rend()) 범위를 사용한다. 두 범위 모두에 대하여 for 회돌이는 동일하다. 그리고 어떻게 auto 키워드로 회돌이 제어 변수의 유형을 정의하는지 보여준다. vector<string>::iterator와 같이 난해하게 변수를 정의하지 않은 점을 눈여겨보라 (3.3.5항):

    #include <iostream>
    #include <vector>
    #include <string>
    using namespace std;

    int main(int argc, char **argv)
    {
        vector<string> args(argv, argv + argc);

        for (auto iter = args.begin(); iter != args.end(); ++iter)
            cout << *iter << " ";
        cout << '\n';

        for (auto iter = args.rbegin(); iter != args.rend(); ++iter)
            cout << *iter << " ";
        cout << '\n';
    }

또 STL은 상수 컨테이너 안의 원소들을 방문할 때 사용해야 하는 const_iterator 상수 반복자 유형을 정의한다. 이전 예제의 벡터 원소들은 변경할 수 있었지만 다음 예제에서는 변경이 불가능하다. 그리고 상수 반복자를 요구한다.

    #include <iostream>
    #include <vector>
    #include <string>
    using namespace std;

    int main(int argc, char **argv)
    {
        vector<string> const args(argv, argv + argc);

        for
        (
            vector<string>::const_iterator iter = args.begin();
                iter != args.end();
                    ++iter
        )
            cout << *iter << " ";
        cout << '\n';

        for
        (
            vector<string>::const_reverse_iterator iter = args.rbegin();
                iter != args.rend();
                    ++iter
        )
            cout << *iter << " ";
        cout << '\n';
    }

이 예제들은 평범한 포인터를 반복자로 사용할 수 있음을 보여준다. vector<string> args(argv, argv + argc)를 초기화하면 args 벡터에 포인터 기반의 반복자 한 쌍이 주어진다. argvargs를 초기화할 첫 인자를 가리킨다. argv + argc는 사용될 마지막 인자 바로 다음을 가리킨다. ++argv는 다음 명령 줄 인자에 도달한다. 이것은 표준 포인터의 특징이다. 이 덕분에 반복자를 기대하는 상황에도 사용할 수 있다.

STL은 다섯 가지 유형의 반복자를 정의한다. 이 반복자 유형은 총칭 알고리즘에서 기대하며 특정 유형의 반복자를 손수 생성하려면 그 특징을 이해하는 것이 중요하다. 일반적으로 반복자는 다음과 같이 정의한다 (22.14절):

19장에 총칭 알고리즘을 기술할 때 다음의 반복자 유형을 사용한다. 무작위 접근 반복자와 함께 제시한 예제는 반복자와 총칭 알고리즘을 관련짓는 법을 보여준다. (총칭) 알고리즘이 요구하는 반복자를 찾은 다음, 데이터 구조가 반복자 유형을 지원하는지 확인한다. 그렇지 않다면 그 알고리즘은 그 데이터 구조에 사용할 수 없다.

18.2.1: std::distance

앞서 18.2절에 언급했듯이 반복자는 메모리에 연속적으로 원소를 저장하고 있는 컨테이너에 대하여 포인터 연산을 지원한다. 하지만 완전하게 맞는 말은 아니다. 두 개의 반복자가 참조하는 원소 사이에 있는 원소의 갯수를 결정하려면 반복자는 뺄셈 연산자를 지원해야 하기 때문이다.

std::list 또는 std::unordered_map에서 포인터 연산을 사용하여 두 반복자 사이의 원소의 갯수를 계산하는 것은 불가능하다. 이런 컨테이너는 원소들이 메모리에 연속적으로 저장되지 않기 때문이다.

std::distance 함수는 이 작은 틈새를 메워 준다. std::distance는 두 개의 입력반복자(InputIterator)를 기대하고 둘 사이에 있는 원소의 갯수를 돌려준다.

distance 함수를 사용하기 전에 먼저 <iterator> 헤더를 포함해야 한다.

첫 인자로 지정된 반복자가 두 번째 인자로 지정된 반복자를 초과하면 원소의 갯수는 양수가 아니다. 그렇지 않으면 음수가 아니다. 원소의 갯수를 결정할 수 없다면 (반복자가 같은 컨테이너에 있는 원소를 참조하지 않으면) distance의 반환 값은 정의되어 있지 않다.

예제:

    #include <iostream>
    #include <unordered_map>
        
    using namespace std;
    
    int main()
    {
        unordered_map<int, int> myMap = {{1, 2}, {3, 5}, {-8, 12}};
    
        cout << distance(++myMap.begin(), myMap.end()) << '\n'; // 출력: 2
    }

18.2.2: 삽입 반복자

총칭 알고리즘은 알고리즘으로 처리된 결과를 집어넣을 목표 컨테이너를 요구하는 경우가 많다. 예를 들어 copy 총칭 알고리즘은 세 개의 매개변수가 있다. 앞에서 두 개는 방문할 원소의 범위가 정의되고 세 번째에 복사 연산의 결과가 저장될 첫 위치가 정의된다.

copy 알고리즘으로 복사될 원소의 갯수는 미리 알 수 있는데 그 갯수를 포인터 연산으로 제공할 수 있기 때문이다. 그렇지만 포인터 연산을 사용할 수 없는 경우가 있다. 예를 들면 결과 원소의 갯수가 최초의 범위에 있던 원소의 갯수와 다를 경우가 있다. unique_copy 총칭 알고리즘이 바로 그런 경우이다. 그런 경우는 목표 컨테이너로 복사되는 원소의 갯수를 미리 알 수 없다.

이런 경우에 inserter 어댑터 함수로 목표 컨테이너에 원소를 삽입할 수 있다. 삽입 어댑터는 세 가지 유형이 있다.

삽입 어댑터는 typedef를 두 가지 요구한다. back_inserter에 집중하여 보자. 이 반복자는 push_back 멤버를 지원하는 컨테이너의 이름을 기대한다. 삽입자의 operator() 멤버는 컨테이너의 push_back 멤버를 호출한다. 다음과 같은 클래스를 인터페이스에 추가하면 push_back 멤버를 지원하는 클래스의 객체는 무엇이든 back_inserter에 인자로 건넬 수 있다.
    typedef DataType const &const_reference;
(DataType const &push_back 멤버 함수에 건네는 매개변수의 유형이다). 예를 들어:
    #include <iostream>
    #include <algorithm>
    #include <iterator>
    using namespace std;

    class Insertable
    {
        public:
            typedef int value_type;
            typedef int const &const_reference;

            void push_back(int const &)
            {}
    };

    int main()
    {
        int arr[] = {1};
        Insertable insertable;

        copy(arr, arr + 1, back_inserter(insertable));
    }

18.2.3: `istream' 객체를 위한 반복자

istream_iterator<Type>를 사용하면 istream 객체의 반복자 집합을 정의할 수 있다. istream_iterator 반복자의 일반적 형태는 다음과 같다.
    istream_iterator<Type> identifier(istream &in)
Typeistream으로부터 읽은 데이터 원소의 유형이다. 반복자 범위에서 `시작' 반복자로 사용된다. Typeoperator>>istream과 함께 정의되어 있기만 하면 어떤 유형이든 상관이 없다.

끝-반복자로 기본 생성자가 사용되고 스트림의 끝에 상응한다. 예를 들어,

    istream_iterator<string> endOfStream;
begin-iterator를 정의할 때 지정했던 스트림 객체는 기본 생성자에 언급하지 않는다.

back_inserteristream_iterator 어댑터를 사용하면 스트림으로부터 얻는 모든 문자열을 컨테이너 안에 손쉽게 저장할 수 있다. 예를 들어 (익명의 istream_iterator 어댑터를 사용하면):

    #include <iostream>
    #include <iterator>
    #include <string>
    #include <vector>
    #include <algorithm>
    using namespace std;

    int main()
    {
        vector<string> vs {"한글", "만세"};

        copy(istream_iterator<string>(cin), istream_iterator<string>(),
             back_inserter(vs));

        for
        (
            vector<string>::const_iterator begin = vs.begin(), end = vs.end();
                begin != end; ++begin
        )
            cout << *begin << ' ';
        cout << '\n';
    }

18.2.3.1: `istreambuf' 객체를 위한 반복자

streambuf 객체에 입력 반복자를 사용할 수도 있다.

입력 연산을 지원하는 streambuf 객체로부터 읽어 들이기 위해 istreambuf_iterator를 사용할 수 있다. 지원하는 연산들은 istream_iterator에서도 사용할 수 있다. istream_iterator 반복자 유형과 다르게 istreambuf_iterator는 세 개의 생성자를 지원한다.

18.2.4.1목istreambuf_iteratorostreambuf_iterator를 모두 사용하는 예제를 보여준다.

18.2.4: `ostream' 객체를 위한 반복자

ostream_iterator<Type> 어댑터를 사용하면 ostream을 OutputIterator를 기대하는 알고리즘에 건넬 수 있다. 두 개의 생성자를 사용하여 ostream_iterator를 정의할 수 있다.
    ostream_iterator<Type> identifier(ostream &outStream);
    ostream_iterator<Type> identifier(ostream &outStream, char const *delim);
Typeostream 안에 삽입될 데이터 원소의 유형이다. 유형에 상관없이 operator<<ostream 객체와 조합하여 정의된다. 뒤의 생성자를 사용하면 delimiter 문자열을 경계로 하여 Type 데이터 원소들을 따로따로 분리할 수 있다. 앞의 생성자는 가름자를 사용하지 않는다.

예제는 어떻게 istream_iteratorostream_iterator를 사용하여 파일의 정보를 또다른 파일에 복사하는지 보여준다. 아마도 여기에서 in.unsetf(ios::skipws)를 사용하고 싶을 수 있다. ios::skipws 깃발을 내리기 위해 사용된다. 결과적으로 공백 문자들은 연산자에 의하여 그대로 반환된다. 파일은 문자 단위로 복사된다. 다음은 그 프로그램이다.

    #include <iostream>
    #include <algorithm>
    #include <iterator>
    using namespace std;

    int main()
    {
        cin.unsetf(ios::skipws);
        copy(istream_iterator<char>(cin), istream_iterator<char>(),
             ostream_iterator<char>(cout));
    }

18.2.4.1: `ostreambuf' 객체를 위한 반복자

streambuf 객체에 출력 반복자를 사용할 수도 있다.

출력 연산을 지원하는 streambuf 객체에 쓰기 위해 ostreambuf_iterator를 사용할 수 있다. ostream_iterator에도 사용할 수 있다. ostreambuf_iterator는 두 개의 생성자를 지원한다.

다음 예제는 스트림을 또다른 방식으로 복사할 때 istreambuf_iteratorostreambuf_iterator를 모두 사용하는 예를 보여준다. streambuf 스트림에 직접적으로 접근하기 때문에 스트림과 스트림 깃발(flag)은 무시된다. 결론적으로 이전 절처럼 ios::skipws를 내릴 필요가 없다. 반면에 다음 프로그램은 아마도 이전 절에 보여준 프로그램보다 훨씬 더 효율적일 것이다.
    #include <iostream>
    #include <algorithm>
    #include <iterator>
    using namespace std;

    int main()
    {
        istreambuf_iterator<char> in(cin.rdbuf());
        istreambuf_iterator<char> eof;
        ostreambuf_iterator<char> out(cout.rdbuf());

        copy(in, eof, out);
    }

18.3: 'unique_ptr' 클래스

이 절에 보여줄 독점 포인터(unique_ptr)를 사용하려면 먼저 <memory> 헤더를 포함해야 한다.

동적으로 할당된 메모리에 포인터로 접근할 때 메모리 누수를 방지하려면 위치를 엄격하게 관리해야 한다. 동적으로 할당된 메모리를 참조하는 포인터 변수가 영역을 벗어나면 그 메모리는 접근할 수 없으며 프로그램은 메모리 누수를 경험한다.

그런 메모리 누수를 방지하려면 위치를 엄격하게 관리해야 하므로 포인터 변수가 영역을 벗어나기 전에 프로그래머는 동적으로 할당된 메모리가 공용 풀에 제대로 반납되는지 확인해야 한다.

동적으로 할당한 한 개의 값이나 객체를 포인터 변수가 가리키면 위치를 관리하기가 대단히 단순해진다. 포인터 변수를 독점 포인터로 정의할 수 있기 때문이다.

독점 포인터는 포인터를 대신하는 객체이다. 객체이기 때문에 영역을 벗어날 때 소멸자가 호출된다. 소멸자는 자신이 가리키는 동적으로 할당된 메모리를 자동으로 삭제한다. 독점 포인터(unique_ptr)와 그의 사촌인 공유 포인터(shared_ptr)는 스마트 포인터라고 불리운다 (18.4절).

독점 포인터는 몇 가지 특징이 있다.

unique_ptr 클래스에서 제공하는 여러 멤버 함수로 포인터 자체에 접근하거나 독점 포인터가 또다른 메모리 블록을 가리키도록 만들 수 있다. 이런 멤버 함수를 그리고 독점 포인터의 생성자를 다음 몇 항에 소개한다.

공유 포인터(shared_ptr)와 더불어 독점 포인터(unique_ptr)는 지금은 비추천된 자동 포인터(auto_ptr)를 대신하여 안전하게 사용할 수 있다 (18.4절). 독점 포인터는 품질도 자동 포인터를 능가한다. 맞춤 소멸자를 추가할 수 있기 때문에 컨테이너 그리고 총칭 알고리즘과 함께 사용할 수 있다. 게다가 배열도 독점 포인터로 처리할 수 있다.

18.3.1: `unique_ptr' 객체 정의하기

독점 포인터를 정의하는 방법은 세 가지가 있다. 각 정의마다 옆꺽쇠 사이에 <type> 지정자를 포함한다.

18.3.2: 평범한 `unique_ptr' 만들기

독점 포인터의 기본 생성자는 특정한 메모리 블록을 가리키지 않는다.
    unique_ptr<type> identifier;
독점 포인터가 제어하는 포인터는 0 (영)으로 초기화된다. unique_ptr 객체 자체는 포인터가 아니지만 그 값은 0비교할 수 있다. 예제:
    unique_ptr<int> ip;

    if (!ip)
        cout << "0-pointer with a unique_ptr object\n";
대안으로, get 멤버를 사용할 수 있다 (18.3.5항).

18.3.3: 또다른 `unique_ptr' 이동하기

독점 포인터는 유형이 같으면 rvalue 참조를 사용하여 초기화할 수 있다.
    unique_ptr<type> identifier(other unique_ptr object);
다음 예제에 이동 생성자가 사용된다.
    void mover(unique_ptr<string> &&param)
    {
        unique_ptr<string> tmp(move(param));
    }
비슷하게 할당 연산자를 사용할 수 있다. unique_ptr 객체는 유형이 같은 임시 unique_ptr 객체에 할당할 수 있다. 역시 이동-의미구조가 사용된다. 예를 들어:
    #include <iostream>
    #include <memory>
    #include <string>

    using namespace std;

    int main()
    {
        unique_ptr<string> hello1(new string("Hello world"));
        unique_ptr<string> hello2(move(hello1));
        unique_ptr<string> hello3;

        hello3 = move(hello2);
        cout << // *hello1 << /\n' <<   // 세그먼트 폴트 에러가 일어날 것임
                // *hello2 << '\n' <<   // 마찬가지
                *hello3 << '\n';
    }
    // 출력:    Hello world

예제가 보여주는 바에 의하면

hello1이나 hello2cout으로 삽입되면 세그먼트 폴트 에러가 일어날 것이다. 그 이유는 이제 명백하다. 0-포인터를 역참조했기 때문이다. 결국, hello3만 실제로 원래 할당된 string을 가리킨다.

18.3.4: 새로 할당된 객체를 가리키기

독점 포인터는 주로 동적으로 할당된 메모리를 가리키는 포인터를 사용하여 초기화된다. 총칭적 형태는 다음과 같다.
    unique_ptr<type [, deleter_type]> identifier(new-expression
            [, deleter = deleter_type()]);
두 번째 deleter(_type) 인자는 선택적이다. 할당된 메모리를 반납하는 자유 함수나 함수객체를 참조한다. 이중 포인터가 할당되어 있고 내포된 포인터마다 방문하여 할당된 메모리를 파괴하는 소멸자이다 (아래 예시 참고).

다음 예제는 문자열 객체를 가리키는 독점 포인터를 초기화한다.

    unique_ptr<string> strPtr(new string("Hello world"));
생성자에 건네어진 인자는 operator new가 돌려준 포인터이다. 유형에 그 포인터가 언급되지 않는 것을 눈여겨보라. 독점 포인터의 생성에 사용된 유형은 new 표현식에 사용된 유형과 똑같다.

다음 예제는 명시적으로 소멸자를 정의하여 어떻게 동적으로 할당된 문자열을 가리키는 포인터 배열을 삭제하는지 보여준다.

    #include <iostream>
    #include <string>
    #include <memory>
    using namespace std;

    struct Deleter
    {
        size_t d_size;
        Deleter(size_t size = 0)
        :
            d_size(size)
        {}
        void operator()(string **ptr) const
        {
            for (size_t idx = 0; idx < d_size; ++idx)
                delete ptr[idx];
            delete[] ptr;
        }
    };
    int main()
    {
        unique_ptr<string *, Deleter> sp2(new string *[10], Deleter(10));

        Deleter &obj = sp2.get_deleter();
    }

독점 포인터를 사용하면 new 표현식에 의해 할당된 실체의 멤버 함수에 도달할 수 있다. 독점 포인터는 마치 동적으로 할당된 객체를 가리키는 평범한 포인터인 것처럼 이런 멤버에 도달할 수 있다. 다음 프로그램은 `hello' 단어 뒤에 `C++' 문자열을 삽입한다.

    #include <iostream>
    #include <memory>
    #include <cstring>
    using namespace std;

    int main()
    {
        unique_ptr<string> sp(new string("Hello world"));

        cout << *sp << '\n';
        sp->insert(strlen("Hello "), "C++ ");
        cout << *sp << '\n';
    }
    /*
        출력:
            Hello world
            Hello C++ world
    */

18.3.5: unique_ptr 클래스 연산자와 멤버

독점 포인터는 다음 연산자를 제공한다.

독점 포인터는 다음의 멤버 함수를 제공한다.

18.3.6: 배열에 `unique_ptr' 객체 사용하기

독점 포인터를 사용하여 배열을 저장할 때 역참조 연산자는 별 의미가 없지만 배열에서 독점 포인터는 인덱스 연산자로부터 혜택을 누린다. 보통의 독점 포인터와 동적으로 할당된 객체 배열을 참조하는 독점 포인터 사이의 구별은 템플릿 특정화를 통하여 실현된다.

동적으로 할당된 배열에 다음 구문을 사용할 수 있다.

이 경우 스마트 포인터의 소멸자는 delete가 아니라 delete[]를 호출한다.

18.3.7: 비추천된 'auto_ptr' 클래스

이 클래스는 이제 추천하지 않는다. 앞으로 C++17 표준에서는 제거될 것이 확실하다.

전통적으로 C++에는 자동 포인터(auto_ptr)가 있다. 자동 포인터는 이동 의미구조를 지원하지 않는다. 그럼에도 자동 포인터를 또다른 객체에 할당하면 오른쪽 객체는 자신의 정보를 잃는다.

독점 포인터는 자동 포인터의 단점이 없다. 결론적으로 자동 포인터는 이제 사용을 권장하지 않는다. 자동 포인터는 다음의 단점이 있다.

이 단점 때문에 그리고 대체할 수 있기 때문에 자동 포인터는 더 이상 C++ 주해서에 다루지 않는다. 기존의 소프트웨어는 독점 포인터나 공유 포인터 같은 스마트 포인터를 사용하도록 변경해야 한다. 그리고 새 소프트웨어는 되도록이면 이 새로운 스마트 포인터 유형의 관점에서 직접적으로 구현해야 한다.

18.4: 'shared_ptr' 클래스

독점 포인터 말고도 공유 포인터(shared_ptr)를 사용할 수 있다. 참조 횟수를 세는 스마트 포인터이다.

공유 포인터를 사용하기 전에 <memory> 헤더를 포함해야 한다.

공유 포인터는 참조 카운터가 0으로 줄어들면 자동으로 그의 내용을 파괴한다. 독점 포인터처럼 shared_ptr<Base>를 사용하여 새로 할당된 Derived 클래스 객체를 저장할 때 반환된 Base *Derived *static_cast를 사용하여 형변환할 수 있다. 다형성은 요구되지 않는다. 공유 포인터를 재초기화하거나 공유 포인터가 영역을 벗어날 때 슬라이싱(객체를 복사하면 파생 객체는 소실되고 바탕 객체만 복사되는 현상 (복사 손실)-재정의 불가능)이 일어나지 않고, Derived의 소멸자가 호출된다 (18.3절).

공유 포인터는 복사 생성자와 이동 생성자는 물론이고 표준 할당 연산자와 중복정의 이동 할당 연산자도 지원한다.

독점 포인터처럼 공유 포인터도 동적으로 할당된 배열을 참조할 수 있다.

18.4.1: `shared_ptr' 객체 정의하기

공유 포인터를 정의하는 방법은 네 가지가 있다. 정의마다 옆꺽쇠 사이에 <type> 지정자가 있다.

18.4.2: 평범한 `shared_ptr' 만들기

기본 생성자는 공유 포인터를 단순하게 정의한다. 특정 메모리 블록을 가리키지 않는다.
    shared_ptr<type> identifier;
공유 포인터가 통제하는 포인터는 0 (영)으로 초기화된다. 공유 포인터 자체는 포인터가 아니지만 그의 값은 0비교할 수 있다. 예를 들어:
    shared_ptr<int> ip;

    if (!ip)
        cout << "0-pointer with a shared_ptr object\n";
다른 방법으로서 get 멤버를 사용할 수도 있다 (18.4.4항).

18.4.3: 새로 할당된 객체를 공유 포인터로 가리키기

공유 포인터는 주로 동적으로 할당된 메모리 블록으로 초기화된다. 총칭적 형태는 다음과 같다.
    shared_ptr<type> identifier(new-expression [, deleter]);
두 번째 인자(deleter)는 선택적이다. 할당된 메모리를 파괴하는 자유 함수나 함수객체를 참조한다. 이중 포인터가 할당되고 내포된 포인터마다 방문하면서 할당된 메모리를 파괴하는 소멸자이다 (아래 예시 참고). 독점 포인터가 마주하는 상황에 사용된다 (18.3.4항).

다음은 문자열 객체를 가리키는 공유 포인터를 초기화하는 예이다.

    shared_ptr<string> strPtr(new string("Hello world"));
생성자에 건네진 인자는 operator new가 반환하는 포인터이다. type에 포인터가 전혀 보이지 않는 것을 눈여겨보라. 공유 포인터 생성에 사용된 유형은 new 표현식에 사용된 유형과 똑같다.

다음 예제는 두 개의 공유 포인터가 실제로 정보를 공유하는 것을 보여준다. 한쪽에서 정보를 변경하면 변경된 정보가 다른 쪽에 보인다.

    #include <iostream>
    #include <memory>
    #include <cstring>
    using namespace std;

    int main()
    {
        shared_ptr<string> sp(new string("Hello world"));
        shared_ptr<string> sp2(sp);

        sp->insert(strlen("Hello "), "C++ ");
        cout << *sp << '\n' <<
                *sp2 << '\n';
    }
    /*
        출력:
            Hello C++ world
            Hello C++ world
    */

18.4.4: 공유 포인터의 연산자와 멤버

공유 포인터는 다음 연산자를 지원한다.

공유 포인터는 다음 멤버 함수를 지원한다.

18.4.5: 공유 포인터 형변환 하기

공유 포인터와 조합하여 표준 C++ 스타일로 유형을 변환할 때 주의할 점이 있다. 다음 두 클래스를 연구해 보자:
    struct Base
    {};
    struct Derived: public Base
    {};

새로 할당된 Derived 클래스 실체를 저장하기 위하여 shared_ptr<Base>를 정의하면 Base * 유형이 반환된다. 이 반환 유형은 단독 포인터처럼 static_cast를 사용하여 Derived *으로 형변환할 수도 있다. 다형성은 요구하지 않는다. 공유 포인터를 재초기화하거나 공유 포인터가 영역을 벗어나더라도 슬라이싱(복사손실)은 일어나지 않는다. 그리고 Derived의 소멸자가 호출된다 (18.3절).

물론 shared_ptr<Derived>은 쉽게 정의할 수 있다. Derived 객체는 Base 객체이기도 하므로 굳이 형변환하지 않아도 Derived를 가리키는 포인터는 Base를 가리키는 포인터로 간주해도 된다. 그러나 static_cast를 사용하면 Derived *Base *로 번역하도록 강제할 수 있다.

    Derived d;
    static_cast<Base *>(&d);

그렇지만 Derived 객체를 가리키는 공유 포인터의 get 멤버를 사용하여 공유 포인터를 Base로 변환할 때 평범한 static_cast는 사용할 수 없다. 다음 코드는 결국 동적으로 할당된 Base 객체를 두 번 파괴하려고 시도하게 될 것이다.

    shared_ptr<Derived> sd(new Derived);
    shared_ptr<Base> sb(static_cast<Base *>(sd.get()));
sdsb는 같은 객체를 가리키기 때문에 sbsd가 영역을 벗어나면 ~Base 소멸자가 두 번 호출된다. 그 때문에 프로그램은 이르게 종료하게 된다. 두 번 해제하는 에러가 일어나기 때문이다.

이 에러는 공유 포인터와 함께 사용되도록 특별히 설계된 형변환을 사용하면 방지할 수 있다. 이 형변환은 특정화된 생성자를 사용한다. 생성된 공유 포인터는 메모리를 가리키지만 소유권을 (즉, 참조 횟수를) 기존의 공유 포인터와 공유한다. 이 특별한 형변환은 다음과 같다.

18.4.6: 배열에 `shared_ptr' 객체 사용하기

독점 포인터와 다르게 공유 포인터는 동적으로 할당된 배열 객체를 다루기 위한 특정화가 존재하지 않는다.

그러나 독점 포인터처럼 공유 포인터로 배열을 참조하면 역참조 연산자는 의미가 없어진다. 대신에 공유 포인터는 인덱스 연산자를 이용할 수 있다.

그런 편의기능을 제공하는 shared_array 클래스를 만드는 것은 어렵지 않다. shared_ptr 클래스로부터 상속받아 shared_array 클래스 템플릿을 만들고 거기에 소멸자만 주면 배열과 그의 원소들을 파괴해 준다. 게다가 인덱스 연산자를 정의하면 선택적으로 delete를 사용하여 역참조 연산자를 선언할 수 있다.

다음은 어떻게 shared_array를 정의하고 사용하는지 보여준다.

    struct X
    {
        ~X()
        {
            cout << "destr\n";  // 객체의 소멸을 보여준다.
        }
    };
    template <typename Type>
    class shared_array: public shared_ptr<Type>
    {
        struct Deleter          // 소멸자는 포인터를 받는다.
        {                       // 그리고 delete[]를 호출한다.
           void operator()(Type* ptr)
           {
              delete[] ptr;
           }
        };
        public:
            shared_array(Type *p)           // 다른 생성자는
            :                               // 여기에 없다.
                shared_ptr<Type>(p, Deleter())
            {}
            Type &operator[](size_t idx)    // 인덱스 연산자
            {
                return shared_ptr<Type>::get()[idx];
            }
            Type const &operator[](size_t idx) const
            {
                return shared_ptr<Type>::get()[idx];
            }
            Type &operator*() = delete;     // 포인터가 없는 멤버는 제거한다.
            Type const &operator*() const = delete;
            Type *operator->() = delete;
            Type const *operator->() const = delete;
    };
    int main()
    {
        shared_array<X> sp(new X[3]);
        sp[0] = sp[1];
    }

18.5: 스마트 포인터 생성: `make_shared' 그리고 `make_unique'

정의 시간에 공유 포인터는 새로 할당된 객체를 가리키는 포인터로 초기화된다. 다음은 예제이다.
    std::shared_ptr<string> sptr(new std::string("hello world"))
이 서술문은 메모리를 두 번 할당한다. 하나는 std::string를 할당하는 데 그리고 다른 하나는 내부적으로 공유 포인터의 생성자 자체에서 사용한다.

make_shared 템플릿을 사용하면 두 번의 할당을 하나의 할당으로 조합할 수도 있다 (shared_ptr의 생성자를 명시적으로 호출하는 것보다 약간 더 효율적이기도 하다). std::make_shared 함수 템플릿은 원형이 다음과 같다.

    template<typename Type, typename ...Args>
    std::shared_ptr<Type> std::make_shared(Args ...args);

make_shared를 사용하기 전에 <memory> 헤더를 포함해야 한다.

이 함수 템플릿은 Type 유형의 객체를 할당하고 args를 생성자에 건네며 (완벽한 전달(perfect forwarding)을 사용, 22.5.2항) 그리고 새로 할당된 Type 객체의 주소로 초기화된 공유 포인터를 돌려준다.

다음은 std::make_shared를 사용하여 위의 sptr 객체를 초기화하는 방법이다. auto 키워드를 눈여겨보라. 덕분에 sptr의 유형을 명시적으로 지정하지 않아도 된다.

    auto sptr(std::make_shared<std::string>("hello world"));
이렇게 초기화하고 나면 std::shared_ptr<std::string> sptr이 정의되고 초기화된다. 다음과 같이 사용할 수 있다.
    std::cout << *sptr << '\n';

C++14 표준은 또 std::make_unique을 제공한다. 이를 make_shared처럼 사용할 수 있지만 공유 포인터가 아니라 독점 포인터를 만들어 준다.

18.6: 포인터 데이터 멤버가 있는 클래스

포인터 데이터 멤버가 있는 클래스는 각별히 신경을 써야 한다. 특히 생성 시간에 날 포인터나 메모리 누수가 발생하지 않도록 특별히 주의를 기울여야 한다. 다음 클래스를 연구해 보자. 두 개의 포인터 데이터 멤버를 정의하고 있다.
    class Filter
    {
        istream *d_in;
        ostream *d_out;
        public:
            Filter(char const *in, char const *out);
    };
Filter 객체는 *d_in으로부터 읽은 정보를 여과해 그 정보를 *d_out에 쓴다고 가정하자. 스트림을 가리키는 포인터를 사용하면 종류에 관계없이 istream이나 ifstream 또는 fstream이나 istringstream 스트림을 필터 객체가 가리키도록 만들 수 있다. 위에 보여준 생성자는 다음과 같이 구현할 수 있다.
    Filter::Filter(char const *in, char const *out)
    :
        d_in(new ifstream(in)),
        d_out(new ofstream(out))
    {
        if (!*d_in || !*d_out)
            throw string("Input and/or output stream not available");
    }
당연히 생성에 실패할 가능성이 있다. new가 예외를 던지거나 스트림 생성자가 예외를 던지거나 또는 스트림이 생성자 몸체에서 예외를 던질 경우 열리지 않을 수도 있다. 함수를 사용하여 try 블록을 시도하면 도움이 된다. d_in의 초기화가 예외를 던지더라도 아무 것도 걱정할 것이 없음을 눈여겨보라. Filter 객체는 생성되지 않았다. 그의 소멸자는 호출되지 않는다. 그리고 예외가 잡힌 곳까지 처리가 계속 진행된다.

그러나 d_out의 초기화나 생성자의 if 서술문에서 예외를 던지면 Filter의 소멸자도 호출되지 않는다. 객체도 없고 그러므로 소멸자도 호출되지 않는다. 이 때문에 메모리 누수가 생길 수 있다. d_ind_out에 대하여 delete가 호출되지 않기 때문이다. 이를 방지하기 위하여 d_ind_out을 먼저 0으로 초기화해야 한다. 그 다음에 초기화를 수행할 수 있다.

    Filter::Filter(char const *in, char const *out)
    try
    :
        d_in(0),
        d_out(0)
    {
        d_in = new ifstream(in);
        d_out = new ofstream(out);

        if (!*d_in || !*d_out)
            throw string("Input and/or output stream not available");
    }
    catch (...)
    {
        delete d_out;
        delete d_in;
    }
그렇지만 이렇게 하면 즉시 복잡해진다. 만약 Filter가 한 클래스의 또다른 데이터 멤버를 품고 있고 그의 생성자가 두 개의 스트림을 필요로 하면 그 데이터는 생성할 수 없다. 아니면 그 자체를 포인터로 변환해야 한다.
    Filter::Filter(char const *in, char const *out)
    try
    :
        d_in(0),
        d_out(0)
        d_filterImp(*d_in, *d_out)    // 작동하지 않는다.
    { ... }

    // 대신에 다음을 사용:

    Filter::Filter(char const *in, char const *out)
    try
    :
        d_in(0),
        d_out(0),
        d_filterImp(0)
    {
        d_in = new ifstream(in);
        d_out = new ofstream(out);
        d_filterImp = new FilterImp(*d_in, *d_out);
        ...
    }
    catch (...)
    {
        delete d_filterImp;
        delete d_out;
        delete d_in;
    }
후자의 대안이 작동하겠지만 혼란스러워진다. 이와 같은 혼란은 스마트 포인터를 사용하여 방지한다. 스트림 포인터를 스마트 포인터 객체로 정의해 생성하면 나머지 생성자의 코드가 예외를 던지더라도 적절하게 파괴된다. FilterImp와 두 개의 단독 포인터를 사용한다면 Filter의 설정과 그의 생성자는 다음과 같다.
    class Filter
    {
        std::unique_ptr<std::ifstream> d_in;
        std::unique_ptr<std::ofstream> d_out;
        FilterImp d_filterImp;
        ...
    };

    Filter::Filter(char const *in, char const *out)
    try
    :
        d_in(new ifstream(in)),
        d_out(new ofstream(out)),
        d_filterImp(*d_in, *d_out)
    {
        if (!*d_in || !*d_out)
            throw string("Input and/or output stream not available");
    }
다시 원래의 구현으로 되돌아 왔다. 그러나 이 번에는 날 포인터나 메모리 누수에 관하여 걱정할 필요가 없다. 초기화 멤버 중 하나가 예외를 던지더라도 (지금은 객체가 되어 있는) 이전에 생성된 데이터 멤버의 소멸자들이 언제나 호출된다.

제일 규칙으로서 클래스가 포인터 데이터 멤버를 정의할 필요가 있을 때 생성자에서 에러를 던질 가능성이 조금이라도 있다면 스마트 포인터로 정의해야 한다.

18.7: 람다 표현식

C++ 람다 표현식을 지원한다. 제 19장에서 보게 되겠지만 총칭 알고리즘은 함수객체나 평범한 함수를 인자로 받는 경우가 많다. 예로는 sort (19.1.58항) 그리고 find_if (19.1.16항) 총칭 알고리즘이 있다. 제일 규칙으로서 호출된 함수가 자신의 상태를 기억하려면 함수객체가 적절하다. 그렇지 않으면 평범한 함수를 사용하면 된다.

함수객체나 함수가 언제나 준비 상태에 있는 것은 아니다. 사용할 곳 근처에 반드시 정의되어 있어야 한다. 이것은 익명의 이름공간에 함수나 클래스를 정의하여 실현한다. (예를 들어 A라는 클래스 또는 함수라면) A를 필요로 하는 코드에 A를 건넨다. 그 코드 자체가 클래스 B의 멤버 함수이면 A의 구현은 클래스 B의 멤버에 접근함으로써 혜택을 누릴 수 있다.

이 체계는 (클래스를 정의하기 때문에) 일반적으로 코드가 폭증하거나 (A의 코드에 자동으로 접근할 수 없는 요소들을 사용가능하도록 만들어야 하기 때문에) 코드가 복잡해진다. 또한 현재 수준의 규격에 맞지 않는 코드를 생산할 수도 있다. 내포 클래스도 이 문제를 해결하지 못한다. 더구나 내포 클래스는 템플릿에 사용할 수 없다.

람다 표현식이 이 문제를 해결해 준다. 람다 표현식은 익명의 (이름없는) 함수객체를 정의한다. 다음 몇 항목에 설명하듯이 함수객체를 인자로 기대하는 함수에 이 익명 함수를 바로 건네도 된다.

18.7.1: 람다 표현식: 구문

람다 표현식은 익명 함수객체를 정의한다. 이를 울타리 객체라고도 부른다. 람다 표현식이 평가될 때 임시(울타리) 객체가 생성된다. 울타리 객체의 유형을 클로저 유형이라고 부른다.

람다 표현식은 블록이나 클래스 또는 이름공간 안에 (즉, 원하는 곳이면 어디든지) 사용된다. 묵시적인 클로저 유형은 람다 표현식을 담고 있는 이름공간 또는 클래스 같은 초소형 블록 안에 정의가 된다. 클로저 객체는 정의가 시작되는 시점에서 보이고 정의가 끝나면 보이지 않는다.

클로저 유형은 (const) 공개 인라인 함수 호출 연산자를 정의한다. 다음은 람다 표현식의 예이다.

    []                      // `람다-도입부'
    (int x, int y)          // `람다-선언부'
    {                       // 보통의 복합 서술문
        return x * y;
    }
이 람다 표현식으로 생성된 클로저 객체의 함수 호출 연산자는 두개의 int 인자를 기대하고 그 곱을 돌려준다. 클로저 유형의 인라인 const 멤버이다. const 속성을 걷어 내려면 람다 표현식에 다음과 같이 mutable을 지정해야 한다.
    [](int x, int y) mutable
    ...
람다-선언부는 매개변수가 없다면 생략해도 된다. 람다 선언부의 매개변수는 기본 인자를 제공하지 않아도 된다.

위의 람다 표현식으로 정의된 클로저 객체를 accumulate (19.1.1항) 총칭 알고리즘과 조합하여 사용하면 벡터에 저장된 일련의 int 값들의 곱을 얻을 수 있다.

    cout << accumulate(vi.begin(), vi.end(), 1,
                [](int x, int y) { return x * y; });
위의 람다 함수는 묵시적으로 decltype(x * y) 유형을 돌려준다. 다음과 같은 경우 유형을 묵시적으로 반환한다.

여러 return 서술문이 있어서 다른 유형의 값을 반환하면 늦게-지정되는 반환 유형을 사용하여 (3.3.5항) 람다 표현식의 반환 유형을 명시적으로 지정해야 한다.

    [](int x, int y) -> int
    {
        return y < 0 ?
                    x / static_cast<double>(y)
                :
                    z + x;
    }

람다 표현식의 위치에서 보이는 변수에 접근할 수 있다. 어떻게 이 변수에 접근할 것인지는 람다-도입부의 내용에 달려 있다 (각괄호 사이의 람다-나포 구역). 람다-나포부로 지역 문맥을 람다 표현식에 건넬 수 있다.

보이는 전역 변수와 정적 변수는 물론 람다 표현식의 복합 서술문 자체에 정의된 지역 변수도 직접적으로 접근해 변경할 수 있다. 예를 들어:

    int global;
    
    void fun()
    {
        []()  // [] 안에 얼마든지 지정할 수 있다.
        { 
            int localVariable = 0;
            localVariable = ++global; 
        };
    }

람다 표현식이 (비-정적) 클래스 멤버 함수 안에 정의되고 람다-나포부에서 처음에 & 또는 = 문자를 사용하면 this 포인터를 사용할 수 있다. 그래서 람다 표현식은 클래스의 모든 멤버에 접근할 수 있다. 이 경우 람다 표현식은 클래스의 데이터 멤버를 변경할 수도 있다.

람다 표현식이 함수 안에 정의되어 있으면 그 시점에 보이는 함수의 모든 지역 변수에 접근할 수 있다.

람다-나포부에 있는 처음의 & 문자는 이 지역 변수에 참조로 접근한다. 그러면 이 변수들은 람다 표현식 안으로부터 변경할 수 있다.

람다-나포부에 있는 처음의 = 문자는 참조되는 지역 변수의 지역 사본을 생성한다. 이 경우 람다 표현식이 mutable 키워드로 정의되어 있으면 지역 사본의 값들은 람다 표현식만 변경할 수 있다는 것에 주목하라. 예를 들어,

    struct Class
    {
        void fun()
        {
            int var = 0;
            [=]() mutable
            {
                ++var;  // fun 함수의 var가 아니라
            }           // 지역 사본을 변경한다.
        }
    }

C++17 표준에서 람다-표현식을 (비-정적) 멤버 함수에 정의할 때 람다-나포부 안에 *this를 포함할 수도 있다. 굳이 지정하지 않더라도 람다 표현식은 묵시적으로 this 포인터를 나포하고, this 포인터에 상대적으로 클래스 멤버에 언제든지 접근할 수 있다. 그러나 멤버를 비동기적으로 호출하면 문제가 야기될 수 있다. 비동기적으로 람다 함수를 호출하면 그 시점에 생애를 마친 그리하여 이미 존재하지 않는 객체의 멤버를 참조할 가능성이 있기 때문이다. 이 잠재적 문제는 [=, *this]와 같이 =으로 시작하고 `, *this'를 람다 나포 안에 사용하면 해결된다 (그리고 변수는 여전히 예와 같이 나포할 수 있다). `, *this'를 지정하면 this가 참조하는 객체가 명시적으로 나포된다: 그 객체의 영역이 끝나더라도 즉시 소멸되지 않지 않고, 그 표현식이 존재하는 동안 람다 표현식이 나포한다. `, *this'를 지정하려면 그 객체는 직접적으로 사용할 수 있어야 한다. 다음 예제를 연구해 보자:

    struct s2 
    {
        double ohseven = .007;
        auto f() 
        {
            return [this] 
                    {
                        return [*this] 
                                {
                                    return ohseven; // OK
                                }
                    }();
        }
        auto g() 
        {
            return [] 
                    {
                      return [*this] 
                        { 
                            // 에러: 바깥의 람다 표현식에
                            // *this가 나포되지 않는다.
                        }; 
                    }();
        }
    };

세밀하게 조율할 수도 있다. 처음에 =가 있고 쉼표로 갈라 &var를 지정하면 언급된 지역 변수들을 사본이 아니라 참조로 처리해야 한다고 알려준다. 처음에 &가 있고 쉼표로 갈라 var를 지정하면 언급된 지역 변수의 지역 사본을 사용해야 한다고 알려준다. 람다 표현식에 mutable 키워드를 주지 않는 한, 이 사본들의 값은 변경할 수 없다.

람다-나포부에 this를 사용하여 세밀하게 조율할 수도 있다. 람다 표현식 둘레의 클래스 멤버에 접근할 수 있도록 해 준다. 예제:

    class Data
    {
        std::vector<std::string> d_names;
        public:
            void show() const
            {
                int count = 0;
                std::for_each(d_names.begin(), d_names.end(),
                    [this, &count](std::string const &name)
                    {
                        std::cout << ++count << ' ' <<
                            capitalized(name) << '\n';
                    }
                );
            }
        private:
            std::string capitalized(std::string name);
    };

람다 표현식이 익명의 함수객체일지라도 변수에 할당할 수 있다. 변수는 auto 키워드를 사용해 정의하기도 한다. 예를 들어,

    auto sqr = [](int x)
               {
                   return x * x;
               };
이런 람다 표현식의 생애는 람다 표현식을 값으로 받는 변수의 생애와 함께 한다.

18.7.2: 람다 표현식 사용하기

람다 표현식의 구문을 다루어 보았으므로 다양한 상황에 어떻게 사용하는지 살펴 보자.

먼저 (이름붙은) 명명 람다 표현식을 연구하자. 이름붙은 람다 표현식은 지역 함수의 틈새에 멋지게 맞아 들어간다. 함수가 자신의 임무 자체보다 개념적으로 낮은 단계에서 계산을 수행할 필요가 있을 때, 이 계산을 별도의 지원 함수에 싸 넣고 필요한 곳에서 그 함수를 호출하는 것이 매력적이다. 지원 함수들은 익명의 이름공간에 정의할 수는 있지만 그러면 요구하는 함수가 클래스 멤버이고 지원 함수도 그 클래스의 멤버에 접근해야 할 경우 즉시 이상해진다.

그렇다면 이름붙은 람다 함수를 사용할 수 있다. 요구하는 함수 안에 정의할 수 있고, 둘레의 클래스에 완전하게 접근할 수 있다. 람다 표현식에 할당된 이름은 둘레의 함수가 호출할 수 있는 함수의 이름이 된다. 다음 예는 숫자 IP 주소를 점으로 구분한 십진 문자열로 변환한다. Dotted 객체로부터 직접적으로 접근할 수도 있다 (간결하게 구현을 모두 클래스 안에 배치함).

    class Dotted
    {
        std::string d_dotted;
        
        public:
            std::string const &dotted() const
            {
                return d_dotted;
            }
            std::string const &dotted(size_t ip)
            {
                auto octet = 
                    [](size_t idx, size_t numeric)
                    {
                        return to_string(numeric >> idx * 8 & 0xff);
                    };

                d_dotted = 
                        octet(3, ip) + '.' + octet(2, ip) + '.' +
                        octet(1, ip) + '.' + octet(0, ip);

                return d_dotted;
            }
    };

다음으로 for_each 같은 총칭 알고리즘의 사용법을 연구해 보자 (19.1.17항).

    void showSum(vector<int> const &vi)
    {
        int total = 0;
        for_each(
            vi.begin(), vi.end(),
            [&](int x)
            {
                total += x;
            }
        );
        std::cout << total << '\n';
    }
람다 표현식에 int total 변수가 주소로 건네진다. 거기에 함수가 직접적으로 접근한다. 매개변수 리스트에 int x만 단순하게 정의되어 있다. 이것은 vi에 저장된 각각의 값으로 연속적으로 초기화된다. 이 총칭 알고리즘이 완료되면 showSumtotal 변수는 벡터의 값을 모두 합한 값을 받는다. 람다 표현식보다 오래 생존하고 그 값이 화면에 표시된다.

그러나 총칭 알고리즘이 대단히 유용함에도 언제나 모든 과업에 딱 맞는 것은 아니다. 게다가 for_each와 같은 알고리즘은 약간 다루기 어려워 보인다. 이제 범위 기반의 for-회돌이를 제공한다. 그래서 위의 구현 대신에 다음을 시도해 보자:

    void showSum(vector<int> const &vi)
    {
        int total = 0;
        for (auto el: vi)
            [&](int x)
            {
                total += x;
            };

        std::cout << total << '\n';
    }
그러나 showSum 함수를 호출하면 cout 서술문이 일관되게 0이라고 보고한다. 무슨 일이 일어난 것인가?

총칭 알고리즘에 람다 표현식을 주면 구현은 함수를 참조로 구체화한다. 그렇게 참조된 함수는 총칭 알고리즘의 내부로부터 호출된다. 그러나 위의 예제에서 범위 기반의 for-회돌이 안에 있는 서술문은 단순히 람다 표현식의 정의만 나타낼 뿐이다. 어떤 것도 실제도 호출되지 않았고 그래서 total은 여전히 0과 같다.

그래서 위의 예제를 작동시키려면 람다 표현식을 정의해야 할 뿐만 아니라 호출도 해야 한다. 람다 함수에 이름을 준 다음에 그 이름으로 호출하면 된다.

    void showSum(vector<int> const &vi)
    {
        int total = 0;
        for (auto el: vi)
        {
            auto lambda = [&](int x)
                            {
                                total += x;
                            };

            lambda(el);
        }
        std::cout << total << '\n';
    }

실제로는 람다 함수에 이름을 줄 필요가 없다. auto lambda 정의면 람다 함수를 나타내고 그것을 직접적으로 호출할 수 있다. 구문이 좀 이상하게 보이지만 아무 잘못도 없다. 지난 예제에 필요했던 복합 서술문을 완전히 생략할 수 있다. 다음을 보자:

    void showSum(vector<int> const &vi)
    {
        int total = 0;
        for (auto el: vi)
            [&](int x)
            {
                total += x;
            }(el);          // 인자 리스트를
                            // 람다 함수의 정의에
                            // 곧바로 추가한다.
        std::cout << total << '\n';
    }

람다 표현식은 condition_variablewait 호출로부터의 가짜 반환을 방지할 수도 있다 (20.5.3항).

condition_variable 클래스로 그렇게 할 수 있다. 잠금과 진위함수를 기대하는 wait 멤버를 제공하면 된다. 진위 함수는 데이터의 상태를 점검하여 데이터 상태가 처리를 허용하면 true를 돌려준다. 다음은 20.5.3항에 보여준 down 멤버를 다르게 구현한 것이다. 데이터를 실제로 사용했는지 점검한다.

    void down()
    {
        unique_lock<mutex> lock(sem_mutex);
        condition.wait(lock, 
            [&]()
            {
                return semaphore != 0
            }
        );
        --semaphore;
    }
람다 표현식은 semaphore가 증가할 때만 wait가 반환된다고 보장한다.

람다 표현식은 주로 프로그램에서 매우 지역화된 부분에 사용되는 함수객체를 얻는다. 기존의 함수 안에서 사용되기 때문에 람다 함수를 사용하면 여러 수준이 집적되어 있다는 사실을 깨닫아야 한다. 함수는 단지 몇 개의 문장만을 사용하여 자신만의 집적 수준에서 기술이 가능한 과업을 구현한다. 예를 들어 ``std::sort 정렬 함수는 호출된 문맥에 적절하게 요소들을 비교함으로써 데이터 구조를 정렬한다''. 기존의 비교 방법을 사용함으로써 집적 수준이 유지되고 그 자체로 명백하게 서술된다. 예를 들어,

    sort(data.begin(), data.end(), greater<DataType>());
기존의 비교 방법을 사용할 수 없다면 맞춤 함수객체를 생성해야 한다. 이것은 람다 표현식을 사용하여 실현할 수 있다. 예를 들어,
    sort(data.begin(), data.end(), 
        [&](DataType const &lhs, DataType const &rhs)
        {
            return lhs.greater(rhs);
        }
    );
뒤의 예제를 보면 두 개의 다른 수준이 집적되어 있음을 깨닫아야 한다. 상위 수준에서 의도는 data의 원소들을 정렬하는 것이다. 그러나 내포 수준에서 (람다 표현식 안에서) 완전히 다른 일이 일어난다. 람다 표현식 안은 두 객체가 어느 쪽이 더 큰지 결정한다. 이런 식으로 여러 수준이 섞여 보이면 읽기에 어려우며 따라서 피하는 게 좋다.

반면에 람다 표현식은 코드를 간단하게 만들어 주기도 한다. 맞춤 함수객체를 정의하는 부담을 피할 수 있기 때문이다. 그러므로 람다 표현식을 아껴서 사용하기를 조언하는 바이다. 람다 표현식을 사용한다면 가능하면 크기를 작게 유지하라. 제일 규칙으로서 람다 표현식은 인라인 함수처럼 취급해야 한다. 단순히 하나의 표현식으로 구성하면 좋고 최대 두 개의 표현식을 넘어서면 좋지 않다.

18.7.3: C++14: 람다 표현식 확장

C++14에서 총칭 람다 표현식이 도입되었다. 총칭 람다 표현식은 auto 키워드로 매개변수를 정의한다. 인자의 실제 유형을 살펴서 적절한 람다 표현식을 생성한다. 총칭적이므로 서로 다른 유형의 인자를 가지고 한 함수 안에서 사용할 수 있다. 다음은 한 예이다 (필요한 모든 헤더와 이름공간이 선언되어 있다고 간주한다):
     1: int main()
     2: {
     3:     auto lambda = [](auto lhs, auto rhs)
     4:                 {
     5:                     return lhs + rhs;
     6:                 };
     7:
     8:     vector<int> values {1, 2, 3, 4, 5};
     9:     vector<string> text {"a", "b", "c", "d", "e"};
    10:
    11:     cout <<
    12:         accumulate(values.begin(), values.end(), 0, lambda) << '\n' <<
    13:         accumulate(text.begin(), text.end(), string(), lambda) << '\n';
    14: }

총칭 람다 함수는 줄 3부터 6까지 정의되어 있고 lambda 식별자에 할당된다. 다음, lambda가 줄 12와 13에서 accumulate에 건네진다. 줄 12에서 int 값을 더하도록 초기화되고 줄 13에서 std::string 값을 더하도록 초기화된다. 같은 lambda가 두 개의 완전히 다른 함수객체(functor)로 초기화된다. main 안에서만 지역적으로 사용할 수 있다.

템플릿을 다루기 전에 맛보기로서 언급하면 (특히 제 21장) 총칭 람다 표현식은 클래스 템플릿과 동등하다. 이를 보여주기 위해 위의 일반화된 람다 함수의 예는 다음과 같은 클래스 템플릿을 사용하여 구현할 수도 있다.

    struct Lambda
    {
        template <typename LHS, typename RHS>
        auto operator()(LHS const &lhs, RHS const &rhs) const 
        {
            return lhs + rhs;
        }
    };
    auto lambda = Lambda{};
이렇게 결과가 동일하게 나오는 이유 하나는 람다 표현식의 매개변수 리스트에 auto 키워드를 사용하면 템플릿 인자 추론 규칙을 따르기 때문이다 (21.4항). 이것은 auto 키워드가 작동하는 방식과 약간 다르다.

C++14 표준에서 람다 표현식이 바깥 영역의 변수를 잡는 방법이 확장되었다. C++11은 값이나 참조로 나포한다. 이 결과 이동 생성만 지원하는 유형의 바깥 영역의 변수는 람다 함수에 값으로 건넬 수 없다. 이 제한은 C++14 표준에서 풀렸으므로 변수들을 어떤 표현식으로든 초기화할 수 있다. 이 덕분에 람다 도입부에서 변수를 이동-초기화할 수 있을 뿐만 아니라 람다 표현식의 바깥 영역에 상응하는 익명의 변수도 여기에서 초기화할 수 있다. 이 경우 다음과 같이 초기화 표현식을 사용할 수 있다.

    auto fun = [value = 1] 
               {
                   return value;
               };
이 람다 함수는 (물론) 1을 돌려준다. 선언된 나포식은 마치 auto 키워드가 사용된 것처럼 초기화 표현식으로부터 유형을 추론한다.

이동 초기화하려면 std::move를 사용해야 한다. 예를 들어,

    std::unique_ptr<int> ptr(new int(10));
    auto fun = [value = std::move(ptr)] 
               {
                   return *value;
               };

총칭 람다 표현식에서 auto 키워드는 람다 함수가 구체화될 때 어느 유형을 사용할지 컴파일러가 결정한다는 것을 나타낸다. 그러므로 총칭 람다 표현식은 생긴 모습은 전혀 다르지만 클래스 템플릿이다 (22장). 다음 람다 표현식은 총칭 클래스 템플릿을 정의한다. 다음과 같이 사용할 수 있다.

    char const *target = "hello";

     auto lambda =         
        [target](auto const &str)
        {
            return str == target;
        };
        
        vector<string> vs{stringVectorFactory()};

        find_if(vs.begin(), vs.end(), lambda);
잘 작동하지만 이런식으로 lambda를 정의하면 복잡한 에러 메시지를 맞을 준비를 해야 한다. 역참조된 반복자의 유형과 (조용하게 추론된) 람다의 str 유형이 일치하지 않을 경우가 있기 때문이다.

18.8: 정규 표현식

C++는 정규 표현식 처리에 필요한 편의기능을 제공한다. C++는 이미 C 유산을 통하여 정규 표현식을 사용할 수 있었다 (C는 항상 regcomp 함수나 regexec 함수를 제공해 왔다). 그러나 C++ 전용의 정규 표현식은 전통적인 C 보다 인터페이스가 더 풍부할 뿐만 아니라 템플릿을 사용하는 코드에도 사용할 수 있다.

C++ 전용의 정규 표현식을 사용하려면 먼저 <regex> 헤더를 포함해야 한다.

정규 표현식은 여기저기에 광범위하게 문서화가 되어 있다 (예를 들어 regex(7), Friedl, J.E.F Mastering Regular Expressions, O'Reilly) (정규 표현식 완전 해부와 실습(개정판) 역 서환수 한빛미디어 출간). 독자는 이런 교재들을 참조하여 정규 표현식이라는 주제를 다시 한 번 상기하시기를 바란다. 본질적으로 정규 표현식은 (`숫자', `식별자', 등등과 같은) 텍스트 단위를 인지하는 작은 메타 언어이다. 토큰과 연관된 입력 문자열을 정의할 때 어휘분석기에 널리 사용된다 (24.8.1항). 그러나 다른 상황에도 크게 쓰인다. sed(1) 그리고 grep(1) 같은 언어는 정규 표현식을 사용하여 파일에서 특정한 텍스트 조각들을 찾는다. perl(1) 같은 언어는`사탕 구문'을 추가하여 정규 표현식을 간단하게 생성한다.

그렇지만 지극히 유용함에도 불구하고 정규 표현식은 또한 몹시 읽기 어려운 것으로도 잘 알려져 있다. 어떤 사람들은 심지어 정규 표현식 언어를 쓰기-전용 언어라고 부르기도 한다. 정규 표현식을 만드는 동안은 왜 특별한 방법으로 작성되었는지 그 이유가 명확하게 드러난다. 그러나 그 반대로 정규 표현식이 무엇을 나타내는지 이해하는 것은 적절한 문맥을 모르면 지극히 어려울 수 있다. 그 때문에 처음부터 꼭 지켜야 할 제일 규칙으로서 정규 표현식의 각 부분마다 무엇에 부합해야 하는지 적절하게 주석을 반드시 붙이도록 하자.

다음 절은 먼저 정규 표현식 언어를 간략하게 개관한다. 다음에 정규 표현식 사용을 위해 C++가 제공하는 편의기능들을 연구한다. 이 편의기능들은 주로 클래스들로 구성된다. 정규 표현식을 지정하는 것을 돕고 정규 표현식을 텍스트에 부합해 보며 그리고 텍스트의 어느 부분이 (있다면) 분석 중인 텍스트의 어느 부분에 부합하는지 알아본다.

18.8.1: 정규 표현식은 미니 언어이다

정규 표현식은 숫자 표현식을 닮은 요소들로 구성된 표현식이다. 정규 표현식은 기본 요소들과 연산자로 구성되며 다양한 우선 순위와 연관 관계가 있다. 숫자 표현식처럼 괄호를 사용해 요소들을 함께 그룹지어서 한 단위를 구성할 수 있다. 거기에 연산자가 작동한다. 광범위한 논의를 보고 싶은 독자는 ecma-international.org 페이지의 15.10절을 참조하는 것이 좋다. 거기에 C++regex 클래스에서 기본으로 사용하는 정규 표현식의 특징이 기술되어 있다.

정규 표현식에 대한 C++의 기본 정의는 다음 아톰(원자 의미 단위)으로 구분한다.

이런 기본적인 아톰 외에도 다음의 특별한 아톰들을 사용할 수 있다 (문자 부류에도 사용할 수 있다).

아톰은 결합해 사용할 수 있다. rs가 아톰이면 정규 표현식 rs는 목표 텍스트가 r에 부합하고 그리고 s에 순서대로 (목표 텍스트 안에 중간 문자 없이) 부합할 경우 목표 텍스트에 부합한다. 예를 들어 정규 표현식 [ab][cd]는 목표 텍스트 ac에 부합하지만 a:c에는 부합하지 않는다.

아톰은 연산자로 결합할 수 있다. 연산자는 앞의 아톰에 묶인다. 연산자가 여러 아톰에 작동해야 한다면 그 아톰들은 괄호로 둘러싸야 한다 (앞의 아톰 설명에서 마지막 항목을 참고). 연산자 문자를 아톰으로 사용하려면 피신시키면 된다. 예를 들어 *는 연산자를 나타내지만 \*는 아톰 별표 문자를 나타낸다. 문자 부류는 피신 연속열을 인지하지 못한다는 것을 유념하라: [\*]는 두 개의 문자로 구성된 문자 부류를 나타낸다. 역사선과 별표 두 개로 구성된 문자 부류이다.

다음 연산자를 지원한다 (rs는 정규 표현식의 아톰을 나타낸다):

정규 표현식에 표식 붙은 부-표현식과 빈도 지정자가 들어 있고 그 부-표현식이 여러 번 부합하면 목표의 최종 부-문자열이 부합한 것으로 보고된다. 예를 들어 regex_search를 사용할 때 (18.8.4.3목) 표식 붙은 부-표현식이 ((a|b)+\s?)이고 목표 텍스트가 a a b이면 a a b가 완전히 부합한 텍스트이다. 반면에 b는 첫 번째와 두 번째 표식붙은 부-표현식에 부합한 부-문자열로 보고된다.

18.8.1.1: 문자 부류

문자 부류 안에서 모든 정규 표현식 연산자는 특별한 의미가 소실된다. 단, \s, \S, \d, \D, \w, 그리고 \W의 특별한 아톰은 제외한다. 문자 범위 연산자 -를 제외하고 문자 부류를 끝내는 연산자 ]를 제외하며 그리고 문자 부류의 시작에 있는 ^를 제외한다. 특수한 아톰과 결합되는 것을 제외하고 피신 문자는 글자 그대로 역사선 문자로 번역된다 (역사선과 d를 담은 문자 부류를 정의하려면 그냥 [d\]를 사용하라).

문자 부류에 닫는 각괄호를 추가하려면 최초의 여는-각괄호 바로 다음에 []를 사용하라. 또는 닫는 각괄호가 담겨 있지 않은 부인된 문자 부류라면 [^]으로 시작하라. 마이너스 문자는 문자 범위를 정의한다 (예를 들어 [a-d][abcd]를 정의함) (실제 범위는 사용중인 로케일에 따라 달라질 수도 있다). 글자 그대로의 마이너스 문자를 문자 부류에 추가하려면 문자 부류의 맨 앞에 ([-, 또는 [^-) 또는 맨 뒤에 배치하라 (-]).

문자 부류가 시작하면 모든 문자들은 마지막 닫는 각괄호 (])에 도달할 때까지 연이어서 문자 집합에 추가된다.

문자와 문자 범위 말고도 문자 부류는 미리 정의된 문자 집합을 담을 수 있다. 다음이 그것이다.

         [:alnum:] [:alpha:] [:blank:]
         [:cntrl:] [:digit:] [:graph:]
         [:lower:] [:print:] [:punct:]
         [:space:] [:upper:] [:xdigit:]
미리 정의된 이 집합은 상응하는 표준 C isXXX 함수에 동등한 문자 집합을 가리킨다. 예를 들어 [:alnum:]isalnum(3)이 참을 돌려주는 모든 문자를 정의한다.

18.8.2: 정규 표현식 정의하기: std::regex

이 항에 제시하는 (w)regex 클래스를 사용하기 전에 <regex> 헤더를 포함해야 한다.

std::regexstd::wregex 유형은 정규 표현식 패턴을 정의한다. 각각 basic_regex<char> 유형과 basic_regex<wchar_t> 유형을 정의한다. 아래 예제에 regex 클래스를 사용하지만 wregex도 사용할 수 있다.

정규 표현식의 기능은 템플릿을 통하여 광범위하게 구현된다. 템플릿은 basic_string<char> 유형을 사용한다 (이것은 std::string과 동등하다). 마찬가지로 OutputIter (출력 반복자)와 BidirConstIter (양방향 상수 반복자)와 같은 총칭 유형이 여러 함수 템플릿과 함께 사용된다. 함수 템플릿은 호출 시간에 건네지는 인자를 보고 실제 유형을 추론한다.

다음은 정규 표현식을 사용할 때 처리하는 단계이다.

regex 객체가 정규 표현식을 처리하는 방식은 std::regex_constants 값을 bit_or로 조합하여 바꿀 수 있다. regex::flag_type 값을 정의한다. 이 regex_constants는 다음과 같다.

생성자

기본적으로 이동 생성자와 복사 생성자를 사용할 수 있다. 기본 생성자는 유형이 regex::flag_type인 매개변수 하나를 정의한다. 기본값은 regex_constants::ECMAScript이다.

멤버 함수

18.8.3: 부합한 것들을 열람하기: std::match_results

regex 객체를 사용할 수 있고 목표 텍스트를 정규 표현식에 맞추어 보려면 다음 항에 기술되는 함수들을 사용할 수 있다 (18.8.4항):

이 함수들은 목표 텍스트와 regex 객체를 (또는 그 객체에 대한 상수 참조를) 제공해야 한다. 부합의 결과를 담기 위해 또다른 인자 std::match_results 객체도 이 함수들에 건넨다.

match_results 클래스를 사용하기 전에 <regex> 헤더를 포함해야 한다.

match_results 객체를 사용하는 예는 18.8.4항에 있다. 이번 항과 다음 항은 주로 참조 목적으로 제공한다.

match_results의 다양한 전문 버전이 존재한다. 전문 버전은 사용된 regex 클래스의 전문 버전에 일치해야 한다. 예를 들어 정규 표현식이 char const *로 지정되어 있으면 match_results 전문 버전도 char const * 값에 작동해야 한다. match_results의 다양한 전문 버전은 쉽게 기억할 수 있도록 이름이 주어졌다. 그래서 적절한 전문 버전을 쉽게 선택할 수 있다.

match_results 클래스는 다음의 전문 버전이 있다.

생성자

기본 복사 생성자와 이동 생성자를 사용할 수 있다. 기본 생성자는 Allocator const & 매개변수를 정의한다. 이 매개변수는 기본 배당자로 초기화된다. match_results 클래스의 객체는 regex_match와 같이 위에 언급된 함수들에 건네어 부합에 관련된 정보를 받는다. 이런 함수들로부터 돌아올 때 match_results 클래스의 멤버를 사용하면 부합 결과를 열람할 수 있다.

멤버 함수

18.8.4: 정규 표현식 부합 객체

이 항에 보여주는 함수를 사용하려면 먼저 <regex> 헤더를 포함해야 한다.

목표 텍스트를 정규 표현식에 맞추어 볼 수 있는 함수 가족이 세 가지 있다. 각 함수와 더불어 match_results::format 멤버는 마지막 인자가 std::regex_constants::match_flag_type 매개변수이다 (다음 목 참고). 이 매개변수는 기본 값이 regex_constants::match_default이다. 이 값을 사용하여 정규 표현식과 사용중인 부합 과정을 세심하게 조율할 수 있다. 이 마지막 매개변수는 정규 표현식 부합 함수나 format 멤버에 명시적으로 언급되지 않는다. 함수 가족 세 가지는 다음과 같다.

regex_replace 다음에 match_results::format 멤버를 사용할 수 있다 (18.8.4.4목). regex_replace를 먼저 다룬다.

18.8.4.1: std::regex_constants::match_flag_type 깃발

모든 중복정의 format 멤버와 모든 정규 표현식 부합 함수는 마지막에 regex_constants::match_flag_type 인자를 받는다. 이 인자는 비트-마스크 유형으로서 bit_or 연산자를 사용할 수 있다. 모든 format 멤버는 기본으로 match_default 인자를 지정한다.

match_flag_type 열거체는 다음 값들을 정의한다 (아래에 `[first, last)' 범위는 부합중인 목표 문자열을 가리킨다).

18.8.4.2: 전체 텍스트에 부합시키기: std::regex_match

std::regex_match 함수는 regex 인자에 정의된 정규 표현식이 목표 텍스트에 완전히 부합하면 true를 돌려준다. 이것은 match_results::prefixmatch_results::suffix가 반드시 빈 문자열을 돌려주어야 한다는 뜻이다. 그러나 부-표현식을 정의하는 것은 문제가 없다.

이 함수의 중복정의 변형은 다음과 같다.

다음은 작은 예제이다. 숫자 5개로 시작한 다음에 알파벳만 있다면 ([[:alpha:]]) 정규 표현식은 목표 텍스트에 부합한다 (argv[1]로 제공). 숫자들은 부-표현식 1로 열람할 수 있다.
    #include <iostream>
    #include <regex>
    
    using namespace std;
    
    int main(int argc, char const **argv)
    {
        regex re("(\\d{5})[[:alpha:]]+"); 

        cmatch results;

        if (not regex_match(argv[1], results, re))
            cout << "No match\n";
        else
            cout << "size: " << results.size() << ": " << 
                    results.str(1) << " -- " << results.str() << '\n';
    }

18.8.4.3: 부분적으로 텍스트에 부합하기: std::regex_search

regex_match와 다르게 std::regex_search 함수는 regex 인자에 정의된 정규 표현식이 목표 텍스트의 한 부분에 부합하면 true를 돌려준다.

이 함수의 중복정의 변형은 다음과 같다.

다음 예제는 regex_search의 사용법을 보여준다.
     1: #include <iostream>
     2: #include <string>
     3: #include <regex>
     4:
     5: using namespace std;
     6:
     7: int main()
     8: {
     9:     while (true)
    10:     {
    11:         cout << "Enter a pattern or plain Enter to stop: ";
    12:
    13:         string pattern;
    14:         if (not getline(cin, pattern) or pattern.empty())
    15:             break;
    16:
    17:         regex re(pattern);
    18:         while (true)
    19:         {
    20:             cout << "Enter a target text for `" << pattern << "'\n"
    21:                     "(plain Enter for the next pattern): ";
    22:
    23:             string text;
    24:             if (not getline(cin, text) or text.empty())
    25:                 break;
    26:
    27:             smatch results;
    28:             if (not regex_search(text, results, re))
    29:                 cout << "No match\n";
    30:             else
    31:             {
    32:                 cout << "Prefix: "  << results.prefix() << "\n"
    33:                         "Match:  "  << results.str()    << "\n"
    34:                         "Suffix: "  << results.suffix() << "\n";
    35:                 for (size_t idx = 1; idx != results.size(); ++idx)
    36:                     cout << "Match " << idx << " at offset " <<
    37:                                 results.position(idx) << ": " <<
    38:                                 results.str(idx) << '\n';
    39:             }
    40:         }
    41:     }
    42: }

18.8.4.4: std::match:_results::format 멤버

format 멤버는 match_results 클래스의 약간 복잡한 멤버 함수이다. 앞서 regex_search 함수로 정규 표현식에 부합한 텍스트를 변경할 수 있다. 복잡하기도 하고 (regex_replace라는) 또다른 정규 표현식 처리 함수가 비슷한 기능을 제공하기 때문에, 먼저 여기에 논의한다. 바로 이어서 regex_replace 함수를 연구한다.

format 멤버는 match_results 객체에 담긴 (부분-) 부합에 작동한다. 형식화 문자열을 사용하고 텍스트를 만들어낸다. 여기에서 ($&와 같은) 형식 지정자들은 원래 제공된 목표 텍스트에 부합한 부분들로 교체된다. 게다가 format 멤버는 모든 표준 C 피신 연속열을 인지한다 (예, \n). format 멤버는 원래 목표 텍스트를 변형한 텍스트를 만들어 낸다.

이를테면 resultsmatch_results 객체이고 match[0]이 (완전히 부합한 텍스트) `hello world'와 같을 경우에 형식화 문자열 this is [$&]를 가지고 format을 호출하면 텍스트 this is [hello world]를 출력한다. 이 형식화 문자열에 $&를 지정한 것을 눈여겨보라. 이것은 형식화 지정자의 한 예이다. 다음은 지원되는 형식화 지정자를 모두 개관한 것이다.

format 멤버는 네 개의 중복정의 버전이 있다. 모든 중복정의 버전은 마지막에 regex_constants::match_flag_type 매개변수를 정의한다. 이 매개변수는 기본으로 match_default로 초기화된다. 이 마지막 매개변수는 다음의 format 멤버에 명시적으로 언급되지 않는다.

format 멤버의 사용법을 더 보여주기 위해 다음 코드를 실행했다고 가정하자.

     1:     regex re("([[:alpha:]]+)\\s+(\\d+)");  // 알파벳 공백 숫자
     2:
     3:     smatch results;
     4:     string target("this value 1024 is interesting");
     5:
     6:     if (not regex_search(target, results, re))
     7:         return 1;

regex_search (줄 6) 함수를 호출한 후, 정규 표현식의 부합 결과는 줄 3에 정의된 match_results 객체에 반환된다.

앞의 두 중복정의 format 함수는 형식화된 텍스트를 쓰기 위하여 출력-반복자를 기대한다. 이런 중복정의 멤버는 마지막에 출력 반복자를 돌려준다. 이 반복자는 이전에 썼던 문자 바로 다음 위치를 가리킨다.

나머지 두 개의 중복정의 format 멤버는 형식화 문자열을 정의하는 std::string이나 NTBS를 기대한다. 두 멤버 모두 형식화된 텍스트를 담은 std::string을 돌려준다.

다음 예제는 string을 어떻게 얻는지 보여준다. 여기에서 이전에 획득한 match_results 객체에 포함된 첫 번째 표식과 두 번째 표식이 붙은 부-표현식의 순서가 서로 바뀐다.
    string reverse(results.format("$2 and $1"));

18.8.4.5: 목표 문자열 변경하기: std::regex_replace

std::regex_replace 함수 가족은 정규 표현식으로 문자열을 교체한다.

기능은 이전 항에서 논의한 match_results::format 멤버를 많이 닮았다. 다음의 중복정의 변형을 사용할 수 있다.

18.9: 확률과 통계적 분포

통계적 분포와 그에 따르는 난수 엔진을 사용하려면 <random> 헤더를 포함해야 한다.

STL은 여러 표준 수학적 (통계적) 분포를 제공한다. 프로그래머는 통계적 분포도에서 무작위로 선택된 값들을 얻을 수 있다.

통계적 분포도는 난수 발생 객체를 제공할 필요가 있다. C 표준 라이브러리에 포함된 전통적인 rand 함수를 확장하여 여러 난수 엔진 객체가 제공된다.

이 엔진들은 의사-난수를 만들어 낸다. 이를 통계 분포에서 처리하여 지정한 분포도로부터 무작위로 선택된 값들을 얻는다.

STL은 다양한 통계 분포를 제공하지만 기능이 많이 부족하다. 확률 밀도 함수나 누적 분포 함수는 현재 STL에서 지원하지 않는다. 그렇지만 이 함수들은 다른 라이브러리에서 얻을 수 있다. 예를 들어 boost 수학 라이브러리가 있다
(특히: http://www.boost.org/doc/libs/1_44_0/libs/math/doc/sf_and_dist/html/index.html).

다양한 통계 분포에 관하여 수학적 특징을 논의하는 것은 이 책의 범위를 벗어난다. 관심이 있는 독자는 수학 교과서를 참고하시기를 바란다 (예, Stuart and Ord's (2009) Kendall's Advanced Theory of Statistics, Wiley) 또는 웹에서 http://en.wikipedia.org/wiki/Bernoulli_distribution를 참고하라.

18.9.1: 난수 엔진

다음 엔진을 사용할 수 있다.

클래스 템플릿 정수/부동소수점수 품질 속도 크기

linear_congruential_engine 정수 보통 보통 1
subtract_with_carry_engine 정수/소수 보통 빠름 25
mersenne_twister_engine 정수 좋음 빠름 624

linear_congruential_engine 난수 엔진은 다음을 계산한다.

valuei+1 = OPENPAa * valuei + c) % m

템플릿 인자를 기대한다. 각각 생성된 난수 값을 담을 데이터 유형과 배율 a와 양의 상수 c 그리고 나머지 연산(modulo) 값 m을 기대한다. 예제:

    linear_congruential_engine<int, 10, 3, 13> lincon;
linear_congruential 엔진에 씨 값을 주려면 생성자에 예를 들어 lincon(time(0))과 같이 씨-인자를 제공하면 된다.

subtract_with_carry_engine 난수 엔진은 다음을 계산한다.

valuei = (valuei-s - valuei-r - carryi-1) % m

템플릿 인자를 기대한다. 발생된 난수 값을 담을 데이터 유형과 나머지 연산(modulo)의 값 m 그리고 음의 상수 sr을 기대한다. 예제:

    subtract_with_carry_engine<int, 13, 3, 13> subcar;
subtract_with_carry_engine 엔진에 씨값을 주려면 생성자에 subcar(time(0))와 같이 씨-인자를 제공하면 된다.

미리 정의된 mersenne_twister_engine mt19937 엔진을 아래 예제에 사용한다 (<random> 헤더 파일에 typedef로 미리 정의되어 있다.). `mt19937 mt'로 생성하거나 아니면 mt19937 mt(time(0))과 같이 생성자의 인자에 씨값을 건네줄 수 있다 (이 엔진은 주기가 219937 - 1이다). mersenne_twister_engine을 초기화하는 다른 방법은 이 책의 범위를 벗어난다 (다음 참고: Lewis et al. ( Lewis, P.A.W., Goodman, A.S., and Miller, J.M. (1969), A pseudorandom number generator for the System/360, IBM Systems Journal, 8, 136-146.) (1969)).

난수 엔진은 seed 멤버를 호출해 씨 값을 뿌릴 수도 있다. seed 멤버는 unsigned long 값이나 발생자 함수를 받는다 (다음과 같이: lc.seed(time(0)), lc.seed(mt)).

난수 엔진은 min 멤버와 max 멤버를 제공한다. 각각 최소값과 최대값(포함)을 돌려준다. 범위를 축소해야 한다면 엔진을 범위에 맞게 함수나 클래스 안에 내포시킬 수 있다.

18.9.2: 통계 분포

다음 항은 C++가 지원하는 다양한 통계 분포를 다룬다. RNG(Random Number Generator)는 난수 발생기를 뜻한다. URNG(Uniform Random Number Generator)는 균등 난수 발생기를 나타낸다. 각 분포마다 모수를 담는 구조체(struct param_type)가 정의되어 있다. param_type 구조체의 조직은 실제 분포에 따라 다르다 (그에 따라 기술한다).

모든 분포는 다음 멤버를 제공한다 (result_type은 분포가 돌려주는 값의 유형 이름을 참조한다):

모든 분포는 다음 연산자를 지원한다 (분포-이름은 normal_distribution과 같이 해당 분포의 이름으로 교체해야 한다.).

다음 예제는 어떻게 분포를 사용하는지 보여준다. (normal_distribution) 분포의 이름을 원하는 분포의 이름으로 바꾸면 다른 분포로 전환된다. 분포는 모두 모수가 있다. 예를 들어 정규 분포라면 표준 편차와 평균이 모수이다. 모수마다 모두 기본 값이 있다. 모수의 이름은 분포마다 다르며 개별적으로 아래에 언급한다. 각 분포마다 모수를 돌려주거나 설정하는 멤버가 있다.

대부분의 분포는 클래스 템플릿으로 정의되어 있다. 그래서 데이터 유형을 함수의 반환 유형에 지정할 필요가 있다. 빈 템플릿 매개변수 유형을 지정하면 (<>) 기본 유형이 자동으로 지정될 것이다. 기본 유형은 (반환 값의 유형이 실수라면) double이거나 아니면 (반환 값의 유형이 정수라면) int이다. 분포가 템플릿 클래스로 정의되어 있지 않으면 템플릿 매개변수 유형을 지정하지 말아야 한다.

다음은 통계 분포의 사용법을 보여주는 예이다. 정규 분포를 적용한다.

#include <iostream>
#include <ctime>
#include <random>
using namespace std;

int main()
{
    std::mt19937 engine(time(0));
    std::normal_distribution<> dist;

    for (size_t idx = 0; idx < 10; ++idx)
        std::cout << "a random value: " << dist(engine) << "\n";

    cout << '\n' <<
        dist.min() << " " << dist.max() << '\n';
}

/* 출력
a random value: 0.810857
a random value: 0.154001
a random value: -0.740865
a random value: -0.196818
a random value: -0.293477
a random value: -2.15427
a random value: 0.614848
a random value: 0.96521
a random value: -0.10051
a random value: -0.616091

-1.79769e+308 1.79769e+308
*/

18.9.2.1: 베르누이(Bernoulli) 분포

bernoulli_distribution는 어떤 p 확률로 논리적 (불리언) 참 값을 생성한다. 한 번의 시행이라면 이항 분포와 동등하다 (18.9.2.2목).

베르누이 분포는 클래스 템플릿으로 정의되어 있지 않다.

정의된 유형:

    typedef bool result_type;
    struct param_type
    {
      explicit param_type(double prob = 0.5);
      double p() const;                     // prob를 돌려준다.
    };

생성자와 멤버:

18.9.2.2: 이항(Binomial) 분포

binomial_distribution<IntType = int>는 연속적으로 n 회의 독립 시행에서 성공할 확률을 결정한다. 각각의 시행마다 p의 확률로 성공한다.

템플릿 유형의 매개변수 IntType은 생성된 난수 값의 유형을 정의한다. 이 유형은 반드시 정수이어야 한다.

정의된 유형:

    typedef IntType result_type;
    struct param_type
    {
      explicit param_type(IntType trials, double prob = 0.5);
      IntType t() const;                    // 시행 횟수를 돌려준다.
      double p() const;                     // 확률을 돌려준다.
    };

생성자와 멤버 그리고 예제:

18.9.2.3: 코시(Cauchy) 분포

cauchy_distribution<RealType = double>는 정규 분포와 비슷하게 생겼다. 그러나 코시 분포는 꼬리가 더 두껍다. 정규 분포를 가정하는 가설을 검정할 때, 코시 분포로 생성된 데이터에 테스트를 해보면 정규분포에 비해 꼬리가 두꺼울수록 테스트가 민감하게 반응하는 것을 볼 수 있다.

코시 분포의 중위값과 표준 편차는 정의되어 있지 않다.

정의된 유형:

    typedef RealType result_type;

    struct param_type
    {
        explicit param_type(RealType a = RealType(0),
                            RealType b = RealType(1));

        double a() const;
        double b() const;
    };

생성자와 멤버:

18.9.2.4: 카이 제곱(Chi-squared) 분포

자유도가 nchi_squared_distribution<RealType = double> 분포는 n 개의 독립적인 표준 정규 무작위 변수들의 제곱을 합한 분포이다.

카이 제곱 분포의 모수 n은 정수 값이지만 반드시 정수일 필요는 없다. 카이 제곱 분포는 실제 인자를 취하는 (expGamma) 함수의 관점에서 정의되기 때문이다 (Gnu g++ 컴파일러 배포본이 제공하는 <bits/random.h> 헤더 파일에 있는 공식들을 참고하라.).

카이 제곱 분포는 관측된 분포가 실험적 분포에 잘 맞는지 테스트할 때 사용된다.

정의된 유형:

    typedef RealType result_type;

    struct param_type
    {
        explicit param_type(RealType n = RealType(1));

        RealType n() const;
    };

생성자와 멤버:

18.9.2.5: 극단 값(Extreme value) 분포

extreme_value_distribution<RealType = double>는 베이불 분포와 관련되어 있다. 그리고 통계 모델에 사용된다. 여기에서 관심의 대상인 변수는 양수 또는 음수 값을 취할 수 있는 수 많은 무작위 요소 중 가장 작은 값이다.

모수가 두 개이다. 위치 모수 a와 배율 모수 b가 있다. 다음도 참고하라
http://www.itl.nist.gov/div898/handbook/apr/section1/apr163.htm

정의된 유형:

typedef RealType result_type;

struct param_type
{
    explicit param_type(RealType a = RealType(0),
                        RealType b = RealType(1));

    RealType a() const;     // 위치 모수
    RealType b() const;     // 배율 모수
};

생성자와 멤버:

18.9.2.6: 지수(Exponential) 분포

exponential_distribution<RealType = double>는 동질적인 포아송 분포로 모델링할 수 있는 사건 사이의 길이를 기술하기 위해 사용된다. 기하 분포의 연속적인 형태로 해석할 수 있다.

모수 prob는 분포의 람다 매개변수를 정의한다. rate 매개변수를 호출한다. 기대 값과 표준 편차는 둘 다 1 / lambda이다.

정의된 유형:

    typedef RealType result_type;

    struct param_type
    {
        explicit param_type(RealType lambda = RealType(1));

        RealType lambda() const;
    };

생성자와 멤버:

18.9.2.7: 피셔 F (Fisher F) 분포

fisher_f_distribution<RealType = double>는 변량 분석과 같은 통계 방법에 주로 사용된다. 두 개의 카이-제곱 분포를 나눈 결과로 나온 분포이다.

두 개의 모수가 특징인데, 각각 두 개의 카이-제곱 분포의 자유도이다.

모수 n은 정수 값이지만 반드시 정수일 필요는 없다. 피셔 F 분포가 카이-제곱 분포로부터 생성되기 때문인데 이 분포가 받는 모수는 정수가 아니다 (18.9.2.4목).

정의된 유형:

    typedef RealType result_type;

    struct param_type
    {
        explicit param_type(RealType m = RealType(1),
                            RealType n = RealType(1));

        RealType m() const; // 나누는 수의 자유도
        RealType n() const; // 나뉘는 수의 자유도
    };

생성자와 멤버:

18.9.2.8: 감마 (Gamma) 분포

gamma_distribution<RealType = double>는 정규 분포에 맞지 않는 데이터와 작업할 때 사용된다. 기다리는 시간을 모델링하는 데 많이 사용된다.

모수가 alphabeta 두 개이다. 기대 값은 alpha * beta이고 표준 편차는 alpha * beta2이다.

정의된 유형:

    typedef RealType result_type;

    struct param_type
    {
        explicit param_type(RealType alpha = RealType(1),
                            RealType beta = RealType(1));

        RealType alpha() const;
        RealType beta() const;
    };

생성자와 멤버:

18.9.2.9: 기하(Geometric) 분포

geometric_distribution<IntType = int>은 (18.9.2.1목) 처음으로 성공할 때까지 필요한 베르누이 시행의 횟수를 모델링하기 위해 사용된다.

모수로 prob가 있고 개별 베르누이 시행의 성공 확률을 나타낸다.

정의된 유형:

    typedef IntType result_type;

    struct param_type
    {
        explicit param_type(double prob = 0.5);
        double p() const;
    };

생성자와 멤버 그리고 예제:

18.9.2.10: 로그-정규(Log-normal) 분포

lognormal_distribution<RealType = double>는 로그를 씌우면 정상 분포인 확률 변수의 확률 분포이다. 확률 변수 X가 정규 분포이면, Y = eX는 로그-정규 분포이다.

모수가 m과 s 두 개이고 각각 ln(X)의 평균과 표준 편차를 나타낸다.

정의된 유형:

    typedef RealType result_type;

    struct param_type
    {
        explicit param_type(RealType m = RealType(0),
                            RealType s = RealType(1));

        RealType m() const;
        RealType s() const;
    };

생성자와 멤버:

18.9.2.11: 정규(Normal) 분포

normal_distribution<RealType = double>는 주로 복잡한 현상을 기술하는 과학에 사용된다. 변량을 예측하거나 측정할 때 에러는 정규적으로 분포되어 있다고 간주된다.

두 개의 모수 평균과 표준 편차가 있다.

정의된 유형:

    typedef RealType result_type;

    struct param_type
    {
        explicit param_type(RealType mean = RealType(0),
                            RealType stddev = RealType(1));

        RealType mean() const;
        RealType stddev() const;
    };

생성자와 멤버:

18.9.2.12: 음의 이항 (Negative binomial) 분포

negative_binomial_distribution<IntType = int> 확률 분포는 지정된 실패 횟수가 일어나기 전에 일련의 베르누이 시행에서 성공한 횟수를 기술한다. 예를 들어 세 번째에 1이 나올 때까지 주사위를 반복적으로 던지면 다른 면이 나올 확률 분포는 음의 이항 분포이다.

모수가 두 개이다. (IntType) k (> 0)는 실험을 멈출 때까지 실패한 횟수이고 (double) p는 각 시행마다 성공할 확률이다.

정의된 유형:

    typedef IntType result_type;

    struct param_type
    {
        explicit param_type(IntType k = IntType(1), double p = 0.5);

        IntType k() const;
        double p() const;
    };

생성자와 멤버:

18.9.2.13: 포아송(Poisson) 분포

poisson_distribution<IntType = int>는 고정된 시간 사이에 일어나는 사건들의 확률을 모델링한다. 이 사건들은 알려진 확률로 일어나고 이전 사건 이후로 시간에 독립적으로 일어난다.

모수는 mean 한 개이다. 일정 시간에 예상되는 사건의 갯수를 지정한다. 예를 들어 일분 동안 평균 2 개의 사건이 발생하고 연구 대상이 10분이면 mean = 20이다.

정의된 유형:

    typedef IntType result_type;

    struct param_type
    {
        explicit param_type(double mean = 1.0);

        double mean() const;
    };

생성자와 멤버:

18.9.2.14: 스튜던트 t(Student t) 분포

student_t_distribution<RealType = double>는 작은 샘플 크기로부터 정규 분포의 모집단의 평균을 예측할 때 사용되는 확률 분포이다.

모수가 한 개인 특징이 있다. 자유도(degrees of freedom)가 그것으로서, 샘플 크기 - 1과 같다.

정의된 유형:

    typedef RealType result_type;

    struct param_type
    {
        explicit param_type(RealType n = RealType(1));

        RealType n() const;    // 자유도
    };

생성자와 멤버:

18.9.2.15: 균등 이산 (Uniform int) 분포

uniform_int_distribution<IntType = int> 균등하게 분포된 정수 값의 범위에서 무작위로 정수 값을 선택한다.

모수가 ab 두 개이고 각각 반환 가능한 최소값과 최대값을 지정한다.

정의된 유형:

    typedef IntType result_type;

    struct param_type
    {
        explicit param_type(IntType a = 0, IntType b = max(IntType));

        IntType a() const;
        IntType b() const;
    };

생성자와 멤버:

18.9.2.16: 균등 연속 (Uniform real) 분포

uniform_real_distribution<RealType = double>는 균등하게 분포된 RealType 값의 범위로부터 무작위로 RealType 값을 선택할 수 있다.

모수가 ab 두 개이고, 각각 돌려줄 수 있는 값의 범위를 지정한다 ([a, b)).

정의된 유형:

    typedef RealType result_type;

    struct param_type
    {
        explicit param_type(RealType a = 0, RealType b = max(RealType));

        RealType a() const;
        RealType b() const;
    };

생성자와 멤버:

18.9.2.17: 베이불(Weibull) 분포

weibull_distribution<RealType = double>는 주로 신뢰 공학과 수명 분석에 사용된다.

두세 개의 매개변수가 있고 STL은 두 개짜리 매개변수 변형을 제공한다. 세 개짜리 매개변수 변형은 모양 (즉, 기울기) 매개변수와 축척 매개변수 그리고 위치 매개변수가 있다. 두 개의 모수 변량은 묵시적으로 모수 값 0의 위치를 사용한다. 두 개짜리 매개변수 변형에는 a 모양 매개변수와 b 축척 매개변수가 제공된다. 베이불의 모수의 의미를 다룬 흥미로운 글은 다음을 참고하라
http://www.weibull.com/hotwire/issue14/relbasics14.htm.

정의된 유형:

    typedef RealType result_type;

    struct param_type
    {
        explicit param_type(RealType a = RealType(1),
                            RealType b = RealType(1));

        RealType a() const;     // 모양 (기울기) 매개변수
        RealType b() const;     // 축척 매개변수
    };

생성자와 멤버:

18.10: std::experimental/filesystem 이름공간

C 언어의 문맥에서 여러 시스템 호출을 사용할 수 있다. 물론 (rename(2), truncate(2), opendir(2), 그리고 realpath(3) 같은) 그런 함수들을 C++에서도 사용할 수 있지만, 서명과 사용법이 별로 마음에 안드는 경우가 많다. 매개변수로 char const *를 요구하고 malloc(3)과 free(3)에 기반하여 정적 버퍼나 메모리 할당을 사용하기 때문이다.

이 함수들을 둘러싼 포장함수를 2003년 이후로 부스트 라이브러리에서 얻을 수 있었다. 현재 C++는 직접적으로 이 편의기능들을 std::(experimental::)filesystem 이름공간에 제공한다. 이 편의기능을 사용하려면 이 헤더를 먼저 포함해야 한다. 그리고 std::(experimental::)filesystem 이름공간으로부터 이 편의기능을 이용하는 프로그램은 stdc++fs 라이브러리를 링크해야 한다: -lstdc++fs.

filesystem 이름공간은 현재 experimental 이름공간 아래에 내포되어 있지만, 모든 기능을 사용할 수 있다. 때가 되면 filesystem 앞의 `실험적'이라는 꼬리표는 떼어내게 될 것이다.

filesystem 이름공간은 광범위하다. 10개 이상의 클래스를 제공하고 30개가 넘는 자유 함수를 지원한다.

이 절과 그 아래의 항에서 fs:: 표기를 사용하여 std::(experimental::)filesystem 이름공간을 가리키겠다.

18.10.1: 파일 시스템 예외: filesystem_error

std::(experimental::)filesystem 이름공간은 따로 filesystem_error 예외 유형을 제공한다. 생성자는 서명이 다음과 같다 (역따옴표가 붙은 매개변수는 선택적이다):
    filesystem_error(string const &what, 
                    [path const &path1, [path const &path2,]] 
                    error_code ec);
filesystem 편의기능은 표준 시스템 기능과 밀접하게 관련되어 있다. errc 에러 코드 열거체 값에서 error_code를 얻어 filesystem_error에 건넨다. 다음의 작은 프로그램으로 이를 보여준다.
    #include <iostream>
    #include <experimental/filesystem>

    namespace fs = std::experimental::filesystem;
    using namespace std;

    int main()
    try
    {
        try
        {
            throw fs::filesystem_error{ "exception encountered", "p1", "p2",
                                        make_error_code(errc::address_in_use) };
        }
        catch (fs::filesystem_error const &fse)
        {
            cerr << fse.what() << ",\n" <<
                    fse.path1() << ",\n" <<
                    fse.path2() << ",\n" <<
                    fse.code() << '\n';

            throw;
        }
    }
    catch (exception const &ec)
    {
        cerr << ec.what() << '\n';
    }

18.10.2: 파일 시스템 개체의 이름: 경로(path)

fs::path 클래스의 객체는 파일 시스템 항목의 이름을 담고 있다. path 클래스는 값-유형의 클래스이다. 기본 생성자(빈 경로)와 더불어 표준 복사/이동 생성/할당 기능을 사용할 수 있다. 그리고 다음 생성자를 사용할 수 있다.

이 생성자들은 (NTBS를 포함하여) 다양한 형태의 문자 연속열을 인자로 기대한다. 개념적으로 이 연속열은 다음 요소로 구성된다 (모두 선택적임).

생성자의 마지막 매개변수는 format ftmp = auto_format인데, 비-기본 규격을 거의 요구하지 않을 것이다. (이 매개변수는 cppreference를 참고하라.)

변경을 담당하는 멤버 함수는 다음과 같다.

(인자도 없고, 불변 멤버인) 접근자는 경로의 내용을 NTBS(c_str)로 돌려준다. 또는 문자열 (string, wstring, u8string, u16string, u32string) (generic_string처럼 앞에 gneric_ 접두사가 붙어 있을 수 있다) 로 돌려주거나 아니면 path로 돌려받은 경로 규격의 구성요소로 돌려준다. 예제:

    fs::path path{ "/usr/local/bin" };
    cout << path.string() << '\n';      // 출력:   /usr/local/bin

경로(path) 객체는 << (스트림 삽입) 연산자를 사용하여 스트림에 삽입된다. 겹따옴표로 경로 이름을 둘러싸 화면에 보여준다. 경로의 내용에 NTBS나 문자열로 접근하면 또는 그것을 문자열에 (형변환하거나) 할당하면 겹따옴표는 제거된다.

경로(path) 객체는 또 >> (스트림 추출) 연산자를 사용하여 스트림으로부터 추출된다. 경로는 선택적으로 겹따옴표로 둘러싸일 수도 있다. 추출된 경로를 또 다시 따옴표가 둘러싼다.

begin 반복자와 end 반복자를 사용하여 path의 모든 구성요소를 반복할 수 있다. 각 구성요소가 path로 반환된다. 맨 처음 루트 이름과 루트 디렉토리가 구성요소로 반환된다. 다음에 개별 디렉토리 (그리고 마지막 파일이름) 구성요소가 따라온다. path의 구성요소를 방문할 때 디렉토리 가름자 자체는 반환되지 않는다.

다음은 객체를 분해하여 돌려주는 구성요소들이다 (요구한 구성요소가 없으면 비어 있다): root_name, root_directory, root_path, relative_path, parent_path (즉, 마지막 요소가 제거된 현재 내용), filename, stem (즉, 점-확장자가 없는 파일이름), 그리고 extension (확장자). 예제:

    fs::path path{ "/usr/local/bin" };
    cout << path.relative_path() << '\n';   // 출력:  "usr/local/bin"
                                            // (겹따옴표에 주목)

앞에 접두사로 has_를 붙이면 멤버는 bool 값을 돌려준다. 구성요소가 존재하면 이 값은 true이다. 다음 멤버도 사용할 수 있다: is_absolute, is_relative

멤버 함수외에도 자유 함수를 사용할 수 있다: ==, !=, <, <=, >, 그리고 >=는 두 개의 path 객체를 비교한다. /lhsrhs를 결합하여 돌려준다. (마치 string 멤버가 돌려주는 반환 값을 비교하는 것처럼) 사전 순서로 비교한다.

(기존의 디렉토리를 참조하는) path를 (경로에 .이나 ..이 담겨 있지 않은) 표준 형태로 변환하려면 canonical 자유 함수를 사용할 수 있다 (18.9.7항).

    fs::path path{ "/usr/local/bin/../../share/man" };
    cout << canonical(path) << '\n';    // 출력:   "/usr/share/man"

18.10.3: 디렉토리 다루기: directory_entry, (recursive_)directory_iterator

directory_entry 클래스의 객체에 디렉토리 엔트리의 이름과 상태가 담긴다. 모든 표준 생성자와 할당 연산자 외에도 path를 기대하는 생성자가 정의되어 있다:
    directory_entry(path const &entry);
entry가 반드시 존재할 필요는 없다.

멤버 함수는 다음과 같다.

또, directory_entry 객체는 ==, !=, <, <=, >, 그리고 >= 연산자를 사용하여 비교할 수 있다. 연산자를 path 객체에 적용한 결과를 돌려준다.

18.10.4: 디렉토리 엔트리에 방문하기: (recursive_)directory_iterator

filesystem 이름공간에 디렉토리를 간단하게 처리해 줄 두 개의 클래스가 있다. directory_iterator 클래스의 객체는 디렉토리 엔트리를 반복하는 (입력) 반복자이다. recursive_directory_iterator 클래스의 객체는 디렉토리의 모든 엔트리를 재귀적으로 반복하는 (입력) 반복자이다.

(recursive_)directory_iterator 클래스는 기본 생성자와 복사 생성자 그리고 이동 생성자를 지원한다. 두 클래스의 객체는 path와 선택적인 error_code로 구성된다. 예를 들어,

    directory_iterator(path const &dest [, error_code &ec]);
이 클래스는 표준 입력 반복자의 모든 멤버를 지원한다 (18.2절). 현재 저장된 경로가 역참조 연산자로 반환된다.
    cout << *fs::directory_iterator{ "/home" } << '\n'; // /home 아래의
                                                        // 첫 엔트리를 보여준다.

이 객체에 부합하는 끝 반복자는 클래스의 기본 생성 객체이다. 그리고 filesystem::beginfilesystem::end 사용할 수 있다. 범위-기반의 for 회돌이에 자동으로 사용된다. 예를 들어 다음 서술문으로 /var/log 디렉토리의 모든 엔트리가 (겹따옴표로 둘러싸여) 화면에 보여진다.

    for (auto &entry: fs:directory_iterator("/var/log"))
        cout << entry << '\n';
대안으로, 명시적으로 반복자를 정의한 for-서술문도 사용할 수 있다. 예를 들어,
    for (
        auto iter = fs:directory_iterator("/var/log"), 
              end = fs::directory_iterator{}; 
                iter != end; 
                    ++iter
    )
        cout << entry << '\n';

fs::(recursive_)directory_iterator base{"/var/log"} 객체는 디렉토리의 첫 요소를 나타낸다. auto &iter = begin(base), auto iter = begin(base), auto &iter = base 또는 auto iter = base처럼 명시적으로 정의된 반복자를 사용할 수 있다. 모두 base의 데이터를 참조한다. 이 반복자를 증가시키면 데이터도 base를 다음 요소로 이동시킨다:

    fs::recursive_directory_iterator base{ "/var/log/" };
    auto iter = base;
                                // 뒤의 두 요소는 경로가 동일하다.
                                // 첫 원소는 경로가 다르다.
    cout << *iter << ' ' << *++iter << ' ' << *base << '\n';    

recursive_directory_iteratordirectory_options 인자도 받는다 (아래 참고). 기본값은 directory_options::none이다.

    recursive_directory_iterator(path const &dest,
                            directory_options options [, error_code &ec]);

enum class directory_optionsrecursive_directory_iterator 객체의 행위를 세밀하게 조율할 수 있는 값들을 정의한다. 비트별 연산자를 지원한다 (심볼 값은 괄호 사이에 보여준다). 다음은 모든 심볼을 개관한 것이다:

directory_iterator 클래스의 멤버와 함께 recursive_directory_iterator 클래스는 다음 멤버들을 더 제공한다:

마지막으로, 다음 데모는 현재 디렉토리와 그 하위 디렉토리의 모든 엔트리를 보여준다.

    int main()
    {
    
        fs::recursive_directory_iterator base{ "/var/log" };

        for (auto entry = base, end = fs::end(base); entry != end; ++entry)
        {
            cout << entry.depth() << ": " << *entry << '\n';
            if (entry.depth() == 1)
                entry.disable_recursion_pending();
        }
    }

18.10.5: 파일 시스템 엔트리의 유형 (file_type)과 접근권한 (perms): file_status

file_status 클래스 객체에 파일 시스템 엔트리의 유형과 접근권한이 담긴다. 복사 생성자와 이동 생성자 그리고 할당 연산자를 사용할 수 있다. 그리고 다음 생성자를 정의한다.
    explicit file_status(file_type type = file_type::none,
                         perms permissions = perms::unknown)
이 생성자는 기본 생성자로 사용할 수도 있다. 그리고 다음 멤버를 정의한다.

enum class file_type은 다음 심볼을 정의한다:

enum class perms는 파일 시스템 엔트리의 모든 접근 권한을 정의한다. 열거체의 심볼은 <sys/stat.h> 헤더 파일에 정의된 상수들보다 의미를 더 잘 기술하도록 선택되었다. 그러나 값은 똑 같다. 비트별 연산자도 모두 enum class perms의 값으로 사용할 수 있다. 다음은 정의된 심볼을 모두 개관한다.

심볼          값      sys/stat.h    의미

none          0000                  접근 권한 비트가 설정되어 있지 않음
                                    
owner_read    0400    S_IRUSR       파일 소유자가 읽기 권한이 있음
owner_write   0200    S_IWUSR       파일 소유자가 쓰기 권한이 있음
owner_exec    0100    S_IXUSR       파일 소유자가 실행/검색 권한이 있음
owner_all     0700    S_IRWXU       파일 소유자가 읽기, 쓰기, 실행/검색 권한이 있음
                                        
group_read    0040    S_IRGRP       파일 그룹이 읽기 권한이 있음
group_write   0020    S_IWGRP       파일 그룹이 쓰기 권한이 있음
group_exec    0010    S_IXGRP       파일 그룹이 실행/검색 권한이 있음
group_all     0070    S_IRWXG       파일 그룹이 읽기, 쓰기, 실행/검색 권한이 있음
                                        
others_read   0004    S_IROTH       기타 사용자가 읽기 권한이 있음
others_write  0002    S_IWOTH       기타 사용자가 쓰기 권한이 있음
others_exec   0001    S_IXOTH       기타 사용자가 실행/검색 권한이 있음
others_all    0007    S_IRWXO       기타 사용자가 읽기, 쓰기, 실행/검색 권한이 있음
                                    
all           0777                  모든 사용자가 읽기, 쓰기, 실행/검색 권한이 있음
                                        
set_uid      04000    S_ISUID       실행시에 사용자 ID를 파일 소유자의 사용자 ID에 부여함
set_gid      02000    S_ISGID       실행시에 그룹 ID를 파일 그룹의 사용자 ID에 부여함
sticky_bit   01000    S_ISVTX       POSIX XSI에 의하면 디렉토리에 설정할 때 
                                            그 디렉토리에 기타 사용자가
                                            (/tmp를 사용하여) 쓸 수 있을지라도
                                            파일 소유자만 파일을 삭제할 수 있다.
                                    
mask         07777                  권한 비트를 모조리 활성화 

다음 작은 프로그램은 파일 상태를 어떻게 결정하고 사용할 수 있는지 보여준다.

    namespace
    {
        std::unordered_map<fs::file_type, char const *> map =
        {
            { fs::file_type::not_found, "an unknown file" },
            { fs::file_type::none,      "not yet or erroneously evaluated "
                                                                "file type" },
            { fs::file_type::regular,   "a regular file" },
            { fs::file_type::directory, "a directory" },
            { fs::file_type::symlink,   "a symbolic link" },
            { fs::file_type::block,     "a block device" },
            { fs::file_type::character, "a character device" },
            { fs::file_type::fifo,      "a named pipe" },
            { fs::file_type::socket,    "a socket file" },
            { fs::file_type::unknown,   "an unknown file type" }
        };

        void status(fs::path const &path)
        {
            fs::file_status stat = fs::directory_entry{ path }.symlink_status();

            cout << path << " is " << map[stat.type()] << '\n';
        };
    } // anon. namespace

    int main()
    {
        for (auto entry: fs::directory_iterator{"/home/frank"})
            ::status(entry);

        cout << "File status_known is " <<
                status_known( fs::file_status{} ) << '\n';
    }

18.10.6: 파일 시스템 엔트리의 공간에 관한 정보: space_info

기존의 path 객체는 특정한 파일 시스템 안에 산다. 파일 시스템은 일정량의 데이터를 담을 수 있다 (바이트의 갯수로 표현됨). 그 중의 일부는 이미 사용중이고 또 일부는 여전히 사용이 가능하다. 이 세가지 정보는 fs::space 함수로 만들 수 있다. fs::path const &를 기대하고, POD struct fs::space_info에 있는 정보를 돌려준다. 이 함수는 filesystem_error를 던진다. path를 첫 인자로 받고 운영 체제의 에러 코드를 error_code 인자로 받는다. 중복정의 space 함수는 error_code 객체를 두 번째 인자로 기대한다. 이 인자는 에러가 일어나지 않으면 소거되고 에러가 일어나면 운영 체제의 에러 코드가 설정된다.

반환된 fs::space_info는 세 개의 필드가 있다:

    uintmax_t capacity;     // 바이트 단위로 총 크기
    uintmax_t free;         // 파일 시스템의 여유 바이트 갯수
    uintmax_t available;    // 프로세스가 점유하지 않은 여유 바이트 갯수
필드를 결정할 수 없으면 -1로 설정된다 (즉, uintmax_t 유형의 최대값이다).

다음 프로그램은 space를 어떻게 사용하는지 보여준다.

    int main()
    {
        fs::path p{ "/tmp" };

        auto pod = fs::space(p);

        std::cout << "The filesystem containing /tmp has a capacity of " <<
                                                pod.capacity << " bytes,\n"
            "i.e., " << pod.capacity / 1024 << " KB.\n"
            "# free bytes: " << pod.free << "\n"
            "# available:  " << pod.available << '\n';
    }

18.10.7: 자유 함수

현재 파일 시스템에 직접적으로 작동하는 여러 함수가 있다.

선택적인 path const &base 매개변수가 기본으로 정의된 함수는 current_path를 사용한다.

그 중에는 error_code &ec 매개변수를 정의한 함수도 있다. 이 함수는 noexcept 규격이 있다. 이 함수가 임무를 완수하지 못하면 ec에 적절한 에러 코드가 설정된다. 에러가 일어나지 않으면 ec.clear()가 호출된다. ec 인자를 제공하지 않으면 이 함수는 임무를 완수하지 못할 경우 filesystem_error를 던진다.

다음 함수를 사용할 수 있다:

파일 유형은 다음 함수로 질의할 수 있다. 모두 다음 서명을 지원한다 (WHATEVER는 요청된 규격이다):

    bool is_WHATEVER(file_status status)
    bool is_WHATEVER(path const path &dest [, error_code &ec])
모든 함수는 요청된 유형에 dest이나 status가 부합하면 true를 돌려준다. 다음은 그 함수들이다:

enum class copy_optionscopy 함수와 copy_file 함수의 행위를 세밀하게 조율할 수 있는 심볼 상수를 정의한다. 이 열거체는 비트별 연산자를 지원한다 (심볼의 값을 괄호 사이에 보여준다). 다음은 정의된 모든 심볼을 개관한다.

파일을 복사할 때의 옵션:

하위 디렉토리를 복사할 때의 옵션:

심링크를 복사할 때의 옵션:

copy 행위를 제어하는 옵션: