제 24 장: 실전 예제

이 장은 클래스와 템플릿의 실전 예제를 보여준다. 가상 함수와 정적 멤버 등등을 보여주겠다. 예제는 대체로 이전 장들의 조직을 따른다.

단순히 C++의 예제를 보여주는 것 말고도 스캐너와 파서 발생기도 다룬다. 이런 도구들이 C++ 프로그램에 어떻게 사용되는지 보여준다. 이런 전통적인 예제들은 여러분이 문법이라든가 파스-트리 그리고 파스-트리 장식 등등과 같이 그 밑에 깔린 개념에 어느 정도 익숙하다고 가정한다. 프로그램의 입력이 일정한 복잡도를 넘어서면 스캐너와 파서 생성기로 코드를 만들어 입력을 처리하는 방법이 더 낫다. 이런 도구를 C++ 환경에서 사용하는 방법을 이 장의 예제 중 하나에 보여준다.

24.1: `streambuf' 클래스로 파일 기술자를 사용하기

24.1.1: 출력 연산을 위한 클래스

파일 기술자로부터 읽고 쓰는 것은 C++ 표준이 아니다. 그러나 대부분의 운영 체제에 파일 기술자를 사용할 수 있고 그것을 장치로 간주할 수 있다. std::streambuf 클래스를 발판으로 삼아 자연스럽게 파일 기술자와 같은 장치와의 인터페이스를 담당할 클래스를 생성해 보자.

아래에서 클래스를 하나 생성해 파일 기술자에 주어진 장치에 써 보겠다. 이 장치는 파일 또는 파이프나 소켓이 될 수도 있다. 24.1.2항은 그런 장치로부터 읽는 법을 다룬다. 24.2.3항은 이전 6.6.2항에서 다룬 바 있는 방향전환을 더 깊게 연구한다.

streambuf를 바탕 클래스로 사용하면 출력 연산용 클래스를 상대적으로 쉽게 설계할 수 있다. 재정의할 유일한 멤버함수는 int streambuf::overflow(int c) 가상 멤버이다. 이 멤버는 문자를 장치에 쓰는 책임을 진다. fd가 출력 파일 기술자이고 그 출력을 버퍼 처리하면 안될 경우에 overflow() 멤버는 그냥 다음과 같이 구현하면 된다.

    class UnbufferedFD: public std::streambuf
    {
        public:
            virtual int overflow(int c);
            ...
    };

    int UnbufferedFD::overflow(int c)
    {
        if (c != EOF)
        {
            if (write(d_fd, &c, 1) != 1)
                return EOF;
        }
        return c;
    }
overflow 멤버함수가 받는 인자는 성공적으로 파일 기술자에 씌여지거나 아니면 EOF가 반환된다.

이 간단한 함수는 출력 버퍼를 사용하지 않는다. 다양한 이유로 출력 버퍼를 사용하는 것은 좋은 생각이다 (다음 절도 참고).

출력 버퍼를 사용하면 overflow 멤버함수는 약간 더 복잡하다. 버퍼가 충만할 때만 호출되기 때문이다. 버퍼가 꽉 차면 먼저 버퍼를 비워야 한다. 버퍼를 비우는 것은 streambuf::sync 가상 함수의 책임이다. sync함수는 가상이므로 streambuf로부터 파생된 클래스는 sync를 재정의해 streambuf가 알지 못하는 버퍼를 비울 수 있다.

sync를 재정의하고 그것을 overflow 안에서 사용하는 것만으로는 부족하다. 버퍼를 정의한 클래스의 실체가 여생을 마칠 때 그 버퍼는 부분적으로만 비워질 수도 있다. 그렇다면 버퍼도 역시 비워야 한다. 이것은 그냥 클래스의 소멸자에서 sync를 호출하기만 하면 해결된다.

이제 출력 버퍼의 중요성을 연구해 보았으므로 우리의 파생 클래스를 설계할 준비가 거의 된 셈이다. 그렇지만 여러 특징을 더 추가해야 한다.

지면을 절약하기 위해 예제 코드에서 설계한 함수들의 완성도는 점검하지 않았다. 물론 `실 세계'의 구현이라면 이런 점검은 생략하면 안 된다. 우리의 OFdnStreambuf 클래스는 다음과 같은 특징이 있다. 다음 프로그램은 OFfdStreambuf 클래스를 사용하여 자신의 표준 입력을 파일 기술자 STDOUT_FILENO에 복사한다. 이 파일 기술자는 표준 출력에 사용되는 심볼이다.
    #include <string>
    #include <iostream>
    #include <istream>
    #include "fdout.h"
    using namespace std;

    int main(int argc, char **argv)
    {
        OFdnStreambuf   fds(STDOUT_FILENO, 500);
        ostream         os(&fds);

        switch (argc)
        {
            case 1:
                for (string  s; getline(cin, s); )
                    os << s << '\n';
                os << "COPIED cin LINE BY LINE\n";
            break;

            case 2:
                cin >> os.rdbuf();      // 다음과 같이 사용해도 됨:  cin >> &fds;
                os << "COPIED cin BY EXTRACTING TO os.rdbuf()\n";
            break;

            case 3:
                os << cin.rdbuf();
                os << "COPIED cin BY INSERTING cin.rdbuf() into os\n";
            break;
        }
    }

24.1.2: 입력 연산을 위한 클래스

입력 연산용 클래스를 std::streambuf으로부터 파생시킬 때 적어도 문자 하나 만큼의 입력 버퍼는 제공해야 한다. 한-문자 입력 버퍼로 istream::putbackistream::ungetc 멤버 함수를 사용할 수 있다. 엄밀히 이야기해 streambuf로부터 파생된 클래스에 버퍼를 구현하는 것이 필수는 아니다. 그러나 버퍼를 사용하는 편이 좋다. 구현이 아주 간단하고 눈에 보이는 그대로 이해되며 클래스의 응용성이 크게 향상되기 때문이다. 그러므로 streambuf으로부터 파생시킨 모든 클래스에 적어도 한 문자 정도의 버퍼는 정의되어 있다.

24.1.2.1: 한 문자 버퍼 사용하기

한 문자 버퍼를 사용하는 streambuf로부터 (IFdStreambuf) 클래스를 상속받을 때 적어도 streambuf::underflow 멤버는 반드시 재정의해야 한다. 이 멤버가 결국 모든 입력 요청에 응답하기 때문이다. streambuf::setg 멤버를 사용하여 streambuf 바탕 클래스에게 입력 버퍼의 위치와 크기를 알린다. 그래서 그에 맞게 입력 버퍼 포인터를 설정할 수 있다. 이렇게 하면 streambuf::eback streambuf::gptr 그리고 streambuf::egptr가 올바른 값을 돌려준다고 확신할 수 있다.

IFdStreambuf 클래스는 다음과 같이 설계한다.

다음 main 함수는 IFdStreambuf를 어떻게 사용하는지 보여준다.
    int main()
    {
        IFdStreambuf fds(STDIN_FILENO);
        istream      is(&fds);

        cout << is.rdbuf();
    }

24.1.2.2: n-문자 버퍼 사용하기

버퍼를 좀 크게 사용하기로 결정하면 얼마나 복잡해질까? 실제로는 그렇게 복잡하지 않다. 다음 클래스로 버퍼의 크기를 지정할 수 있다. 그러나 그것 말고는 기본적으로 이전 절에서 개발한 IFdStreambuf 클래스와 같다. 좀 흥미롭게 하기 위해 일련의 문자를 읽는 것을 최적화해보자. 여기에서 개발된 IFdNStreambuf 클래스에 있는 streambuf::xsgetn 멤버로 재정의한다. 또한 기본 생성자를 제공한다. open 멤버와 조합해 사용하면 파일 기술자를 사용할 수 있기도 전에 istream 객체를 생성할 수 있다. 그 경우에 기술자를 사용할 수 있으면 open 멤버를 사용하여 객체의 버퍼를 초기화할 수 있다. 나중에 24.2절에서 그런 상황을 만나 보겠다.

지면을 절약하기 위해 다양한 호출의 성공 여부는 점검하지 않았다. 물론`실 세계'의 구현이라면 이런 점검을 빼먹으면 안 된다. IFdNStreambuf 클래스는 다음과 같은 특징이 있다.

xsgetn 멤버 함수는 streambuf 객체의 streambuf::sgetn 멤버에 의하여 호출된다. 다음은 이 멤버 함수를 IFdNStreambuf 객체와 함께 사용하는 법을 보여주는 예이다.
    #include <unistd.h>
    #include <iostream>
    #include <istream>
    #include "ifdnbuf.h"
    using namespace std;

    int main()
    {
                                    // 내부적으로: 30개의 문자 버퍼임
        IFdNStreambuf fds(STDIN_FILENO, 30);

        char buf[80];               // main()는 문자 80개를
                                    // 한 블록으로 읽음
        while (true)
        {
            size_t n = fds.sgetn(buf, 80);
            if (n == 0)
                break;
            cout.write(buf, n);
        }
    }

24.1.2.3: `streambuf' 객체에서 위치 찾기

장치가 위치 찾기 연산을 지원하면 std::streambuf로부터 상속된 클래스는 streambuf::seekoff 멤버와 streambuf::seekpos 멤버를 재정의해야 한다. 이 목에서 개발한 IFdSeek 클래스를 사용하면 찾기 연산을 지원하는 장치로부터 정보를 읽을 수 있다. IFdSeek 클래스는 IFdStreambuf로부터 파생되었다. 그래서 딱 한 문자짜리 버퍼를 사용한다. IFdSeek 클래스에 추가된 위치 찾기 연산은 요구받을 때마다 입력 버퍼를 재설정해야 한다. 이 클래스는 또 IFdNStreambuf 클래스로부터 파생시킬 수도 있다. 그 경우에 남아있는 입력 버퍼 다음을 두 번째 세 번째 매개변수가 가리키도록 입력 버퍼를 재설정해야 한다. IFdSeek의 특징을 한 번 살펴 보자: 다음은 IFdSeek 클래스를 사용하는 프로그램의 예이다. 입력을 방향전환하여 이 프로그램에 소스 파일을 넘기면 찾기가 지원된다 (그리고 첫 줄은 제외하고 다른 줄들은 매 줄마다 두번 보여준다):
    #include "fdinseek.h"
    #include <string>
    #include <iostream>
    #include <istream>
    #include <iomanip>
    using namespace std;

    int main()
    {
        IFdSeek fds(0);
        istream is(&fds);
        string  s;

        while (true)
        {
            if (!getline(is, s))
                break;

            streampos pos = is.tellg();

            cout << setw(5) << pos << ": `" << s << "'\n";

            if (!getline(is, s))
                break;

            streampos pos2 = is.tellg();

            cout << setw(5) << pos2 << ": `" << s << "'\n";

            if (!is.seekg(pos))
            {
                cout << "Seek failed\n";
                break;
            }
        }
    }

24.1.2.4: `streambuf' 객체 안의 여러 `unget' 호출

streambuf 클래스와 그의 파생 클래스들은 적어도 마지막 읽은 문자를 버리는 기능을 지원해야 한다. 일련의 unget 호출을 지원해야 할 때 특별히 주의를 기울여야 한다. 이 절에서는 istream::unget 호출 또는 istream::putback 호출 횟수를 지정할 수 있는 클래스의 생성을 연구한다.

여러 (`n'회의) unget 호출 지원은 입력 버퍼의 앞 부분을 예약함으로써 구현된다. 예약된 부분은 마지막으로 읽은 n개의 문자를 담기 위하여 점차로 채워진다. 클래스는 다음과 같이 구현된다.

FdUnget을 사용하는 예제

다음 예제 프로그램은 FdUnget 클래스의 사용법을 보여준다. 최대 10개의 문자를 표준 입력으로부터 읽는다. EOF에서 멈춘다. 2 문자 짜리의 보장된 언겟-버퍼는 3 문자를 담은 버퍼에 정의된다. 한 문자를 읽기 바로 전에 프로그램은 최대 6개의 문자를 버리려고 시도한다. 물론 이것은 불가능하다. 그러나 멋지게도 프로그램은 읽은 문자의 실제 갯수를 고려하여 얼마든지 문자를 버린다.

    #include "fdunget.h"
    #include <string>
    #include <iostream>
    #include <istream>
    using namespace std;

    int main()
    {
        FdUnget fds(0, 3, 2);
        istream is(&fds);
        char    c;

        for (int idx = 0; idx < 10; ++idx)
        {
            cout << "after reading " << idx << " characters:\n";
            for (int ug = 0; ug <= 6; ++ug)
            {
                if (!is.unget())
                {
                    cout
                    << "\tunget failed at attempt " << (ug + 1) << "\n"
                    << "\trereading: '";

                    is.clear();
                    while (ug--)
                    {
                        is.get(c);
                        cout << c;
                    }
                    cout << "'\n";
                    break;
                }
            }

            if (!is.get(c))
            {
                cout << " reached\n";
                break;
            }
            cout << "Next character: " << c << '\n';
        }
    }
    /*
        'echo abcde | program'
        위와 같이 실행한 후의 출력:

        after reading 0 characters:
                unget failed at attempt 1
                rereading: ''
        Next character: a
        after reading 1 characters:
                unget failed at attempt 2
                rereading: 'a'
        Next character: b
        after reading 2 characters:
                unget failed at attempt 3
                rereading: 'ab'
        Next character: c
        after reading 3 characters:
                unget failed at attempt 4
                rereading: 'abc'
        Next character: d
        after reading 4 characters:
                unget failed at attempt 4
                rereading: 'bcd'
        Next character: e
        after reading 5 characters:
                unget failed at attempt 4
                rereading: 'cde'
        Next character:

        after reading 6 characters:
                unget failed at attempt 4
                rereading: 'de
        '
         reached
    */

24.1.3: istream 객체로부터 크기가 고정된 필드 추출

istream 객체로부터 정보를 추출할 때 표준 추출 연산자인 operator>>가 이 작업에 딱 맞춤이다. 추출된 필드는 대부분 서로 공백으로 명확하게 구분되기 때문이다. 그러나 이것이 모든 상황에 들어 맞는 것은 아니다. 예를 들어 웹-폼을 처리 스크립트나 프로그램에 보내면 프로그램은 폼 필드의 값을 url-인코드된 문자로 받는다. 기호와 숫자는 그대로 전송된다. 공백은 + 문자로 전송되고 다른 모든 문자는 %로 시작한 다음에 그 문자의 ascii-값이 두 자리 십육진수로 표현되어 전송된다.

url-인코드된 정보를 해독할 때 간단한 십육진 추출은 작동하지 않는다. 그러면 단지 문자 두 개가 아니라 되도록이면 많은 십육진 문자를 추출하기 때문이다. a-f`0-9 사이의 문자는 적법한 십육진 문자이기 때문에 My name is `Ed'와 같은 텍스트는 다음과 같이 url-인코드된다.

    My+name+is+%60Ed%27
결과적으로 6027이 아니라 십육진 값 60ed 그리고 27이 추출된다. 이름 Ed는 시야로부터 사라진다. 이것이 우리가 원하는 것은 아니다.

이 경우에 %를 보았으므로 istringstream 객체에 배정된 2 문자를 추출할 수 있다. 그리고 istringstream 객체로부터 십육진 값을 추출한다. 약간 귀찮지만 할 만하다. 다른 접근법도 역시 가능하다.

Fistream 클래스는 크기가 고정된 필드에 대하여 istream 클래스를 정의한다. 이 클래스는 (비형식 read 호출은 물론이고) 고정된 크기의 필드 추출 그리고 공백으로 구분된 추출을 모두 지원한다. 이 클래스는 기존의 istream을 감싼 포장 클래스로 초기화할 수 있다. 아니면 기존의 파일이름을 사용하여 초기화할 수 있다. 이 클래스는 istream으로부터 파생된다. 일반적으로 istream이 지원하는 모든 추출 연산을 허용한다. Fistream은 다음의 데이터 멤버를 정의한다.

다음은 Fistream의 클래스 인터페이스의 처음 부분이다.
    class Fistream: public std::istream
    {
        std::unique_ptr<std::filebuf> d_filebuf;
        std::streambuf *d_streambuf;
        std::istringstream d_iss;
        size_t d_width;

언급한 바와 같이 Fistream객체는 파일이름이나 기존의 istream 객체로부터 생성할 수 있다. 그러므로 클래스 인터페이스는 두 개의 생성자를 선언한다.

            Fistream(std::istream &stream);
            Fistream(char const *name,
                std::ios::openmode mode = std::ios::in);

기존의 istream 객체를 사용하여 Fistream 객체를 생성할 때 Fistreamistream 부분은 단순히 streamstreambuf 객체를 사용한다.

Fistream::Fistream(istream &stream)
:
    istream(stream.rdbuf()),
    d_streambuf(rdbuf()),
    d_width(0)
{}

파일이름으로 fstream 객체를 생성할 때 streambuf로 사용될 filebuf 객체가 istream 바탕 초기화자에 새로 주어진다. 클래스의 데이터 멤버는 클래스의 바탕 클래스가 생성되기 전에 먼저 초기화되지 않기 때문에 d_filebuf는 그 다음에야 겨우 초기화될 수 있다. 그 때까지 filebuf는 그냥 streambuf를 돌려주는 rdbuf로만 사용할 수 있을 뿐이다. 그렇지만 실제로는 filebuf이므로 static_cast를 사용하면 rdbuf가 돌려주는 streambuf 포인터를 filebuf *로 유형을 변환할 수 있다. 그래서 d_filebuf를 초기화할 수 있다.

Fistream::Fistream(char const *name, ios::openmode mode)
:
    istream(new filebuf()),
    d_filebuf(static_cast<filebuf *>(rdbuf())),
    d_streambuf(d_filebuf.get()),
    d_width(0)
{
    d_filebuf->open(name, mode);
}

24.1.3.1: 멤버 함수와 예제

setField(field const &) 공개 멤버가 하나 추가되었다. 이 멤버는 추출할 다음 필드의 크기를 정의한다. 그의 매개변수는 field 클래스에 대한 참조이다. 이 클래스는 다음 필드의 너비를 정의하는 조작자 클래스이다.

field &Fistream의 인터페이스에 언급되어 있으므로 field를 먼저 선언해야 비로서 Fistream의 인터페이스가 시작된다. field 클래스 자체는 단순하며 Fistream을 친구로 선언한다. 두 개의 데이터 멤버가 있다. d_width는 다음 필드의 너비를 지정한다. d_newWidthd_width의 값이 실제로 사용되어야 한다면 true로 설정된다. d_newWidthfalse이면 Fistream은 표준 추출 모드로 돌아간다. field 클래스는 생성자가 두 개이다. 기본 생성자는 d_newWidthfalse로 설정하고 두 번째 생성자는 추출할 다음 필드의 너비를 자신의 값으로 기대한다. 다음은 field 클래스의 구현이다.

    class field
    {
        friend class Fistream;
        size_t d_width;
        bool     d_newWidth;

        public:
            field(size_t width);
            field();
    };

    inline field::field(size_t width)
    :
        d_width(width),
        d_newWidth(true)
    {}

    inline field::field()
    :
        d_newWidth(false)
    {}

field 클래스는 Fistream 클래스를 친구로 선언하기 때문에 setField 멤버는 field의 멤버들을 직접적으로 들여다 볼 수 있다.

이제 setField로 돌아갈 시간이다. 이 함수는 field 객체를 참조로 기대한다. 세 가지 다른 방식 중 하나로 초기화된다.

다음은 setField의 구현이다.
std::istream &Fistream::setField(field const &params)
{
    if (params.d_newWidth)                  // 새로운 필드 크기가 요구됨
        d_width = params.d_width;           // 새로운 너비를 설정한다.

    if (!d_width)                           // 너비가 없다면?
        rdbuf(d_streambuf);                 // 예전 버퍼로 되돌아간다.
    else
        setBuffer();                        // 추출 버퍼를 정의한다.

    return *this;
}

비밀 setBuffer 멤버는 d_width + 1개의 문자 버퍼를 정의하고 read를 사용하여 그 버퍼를 d_width개의 문자로 채운다. 버퍼는 NTBS(0으로 끝나는 문자열)이다. 이 버퍼를 사용하여 d_iss 멤버를 초기화한다. Fistreamrdbuf 멤버로 Fistream 객체 자체를 통하여 d_str의 데이터를 추출한다.

void Fistream::setBuffer()
{
    char *buffer = new char[d_width + 1];

    rdbuf(d_streambuf);                         // istream의 버퍼에
    buffer[read(buffer, d_width).gcount()] = 0; // d_width 개의 문자를 읽는다.
                                                // 문자열은 0-바이트로 끝난다.
    d_iss.str(buffer);
    delete[] buffer;

    rdbuf(d_iss.rdbuf());                       // 버퍼를 전환한다.
}

setField를 사용하면 Fistream이 고정 크기로 필드를 추출할지 말지 설정해 줄 수 있지만 아마도 조작자를 사용하는 편이 더 좋을 것이다. field 객체를 조작자로 사용하기 위해 추출 연산자를 중복정의했다. 이 추출 연산자는 istream &field const & 객체를 받는다. 이 추출 연산자를 사용하면 다음과 같은 서술문이 가능하다.

fis >> field(2) >> x >> field(0);
(fisFistream 객체라고 간주한다). 다음은 중복정의 operator>>와 더불어 그의 선언이다.
istream &std::operator>>(istream &str, field const &params)
{
    return static_cast<Fistream *>(&str)->setField(params);
}

선언:

namespace std
{
    istream &operator>>(istream &str, FBB::field const &params);
}

마지막으로 예제이다. 다음 프로그램은 Fistream 객체를 사용하여 표준 입력에 나타나는 url-인코드된 정보를 url-디코드한다.

    int main()
    {
        Fistream fis(cin);

        fis >> hex;
        while (true)
        {
            size_t x;
            switch (x = fis.get())
            {
                case '\n':
                    cout << '\n';
                break;
                case '+':
                    cout << ' ';
                break;
                case '%':
                    fis >> field(2) >> x >> field(0);
                // 최후의 대비책
                default:
                    cout << static_cast<char>(x);
                break;
                case EOF:
                return 0;
            }
        }
    }
    /*
        다음 명령어를 실행한 후의 출력:
            echo My+name+is+%60Ed%27 | a.out

        My name is `Ed'
    */

24.2: `fork' 시스템 호출

C 언어의 fork 시스템 호출은 유명하다. 프로그램이 프로세스를 새로 시작할 필요가 있다면 system 함수를 사용할 수 있다. system 함수는 프로그램에게 자손 프로세스가 끝나기를 기다리도록 요구한다. 부프로세스를 부화시키는 더 일반적인 방식은 fork를 사용하는 것이다.

이 절은 어떻게 하면 fork 같은 복잡한 시스템 호출을 C++로 둘러싸 클래스로 포장할 수 있는지 알아본다. 이 절에서 다른 것들은 대부분 유닉스 운영 체제에 직접적으로 적용된다. 그러므로 유닉스 운영체제에 연구의 초점을 둔다. 다른 시스템도 비슷한 편의기능을 제공한다. 다음에 논의하는 것들은 템플릿 디자인 패턴에 밀접하게 관련된다 (참고: Gamma et al. (1995) Design Patterns, Addison-Wesley)

fork를 호출하면 현재 프로그램이 메모리에 복제된다. 그래서 새로 프로세스를 생성한다. 이렇게 복제한 다음에 두 프로세스는 fork 시스템 호출 아래에서 각자 실행을 계속한다. 두 프로세스는 fork의 반환 값을 조사할 수 있다. 원래의 부모 프로세스의 반환 값은 새로 생성된 자손 프로세스의 반환 값과 다르다.

24.2.1: 기본적인 Fork 클래스

우리가 개발할 Fork 클래스는 기본적으로 fork 호출 같은 시스템의 모든 세부 사항을 사용자로부터 감추어야 한다. 클래스 자체는 적절하게 fork 시스템 호출을 실행할 뿐이다. 일반적으로 fork는 자손 프로세스를 시작하기 위하여 호출된다. 따로따로 프로세스를 실행하는 것이 그 본질이다. 이 자손 프로세스는 표준 입력 스트림에서 입력을 기대하고 표준 출력 또는 에러 스트림에 출력할 수 있다. Fork는 이 모든 것을 알지 못하며 자손 프로세스가 무엇을 하는지 알 필요도 없다. Fork 객체는 자손 프로세스를 기동시킬 수 있어야 한다.

Fork의 생성자는 자손 프로세스가 어떤 행위를 수행해야 하는지 알 수 없다. 마찬가지로 부모 프로세스가 무엇을 수행해야 하는지도 알 수 없다. 이런 상황을 위하여 템플릿 메쏘드 디자인 패턴이 개발되었다. 감마(Gamma c.s.)에 따르면 템플릿 메쏘드 디자인 패턴은

``한 연산의 알고리즘의 뼈대를 정의한다. 몇몇 단계는 부클래스에 넘긴다. [이] 템플릿 메쏘드 (디자인 패턴)은 부클래스에게 알고리즘의 특정 단계를 재정의하도록 허용한다. 알고리즘의 구조는 바뀌지 않는다.''

이 디자인 패턴으로 추상 바탕 클래스를 정의할 수 있다. 이미 fork 시스템 호출과 관련된 핵심 단계는 제공하고 fork 시스템 호출의 다른 부분은 부클래스에게 구현을 미룰 수 있다.

Fork 추상 바탕 클래스는 다음의 특징이 있다.

24.2.2: 부모와 자손

fork 멤버 함수는 fork 시스템 함수를 호출한다 (시스템 함수 fork는 이름이 같은 멤버 함수가 호출하므로 :: 영역 지정 연산자를 사용해야 멤버 함수 자신을 재귀적으로 호출하는 것을 방지할 수 있다). ::fork 함수의 반환 값은 parentProcess가 호출될지 아니면 childProcess가 호출될지 결정한다. 아마도 방향전환이 필요할 것이다. Fork::fork의 구현은 childProcess를 호출하기 바로 전에 childRedirections을 호출하고 parentProcess를 호출하기 바로 전에 parentRedirections를 호출한다.
    #include "fork.ih"

    void Fork::fork()
    {
        if ((d_pid = ::fork()) < 0)
            throw "Fork::fork() failed";

        if (d_pid == 0)                 // 자손 프로세스는 pid가 0이다.
        {
            childRedirections();
            childProcess();
            exit(1);                    // 여기까지 오면 안된다.
        }                               // childProcess()는 종료한다.

        parentRedirections();
        parentProcess();
    }

fork.cc에서 클래스의 내부 fork.ih 헤더 파일이 포함된다. 이 헤더 파일이 필요한 시스템 파일을 책임지고 포함한다. 뿐만 아니라 fork.h 자체도 포함한다. 그의 구현은 다음과 같다.

    #include "fork.h"
    #include <cstdlib>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/wait.h>

자손 프로세스는 반환되면 안 된다. 자신의 일을 끝내면 일단 종료되어야 한다. 자손 프로세스가 exec... 가족의 멤버에 대한 호출을 수행할 때 이런 일이 자동으로 일어난다. 그러나 자손 자체가 여전히 살아 있으면 적절하게 자손이 종료되었는지 확인해야 한다. 자손 프로세스는 exit을 사용하여 자신을 종료한다. 그러나 exit exit이 호출된 레벨과 같거나 더 바깥쪽 레벨에 정의된 객체의 소멸자가 활성화되지 못하도록 막는다. 전역적으로 정의된 객체의 소멸자는 exit이 사용될 때 활성화된다. exit 함수를 사용하여 childProcess를 끝낼 때, 필요한 모든 내포된 객체를 정의한 지원 멤버 함수를 직접 호출하거나 아니면 모든 객체를 (예를 들어 throw 블록을 사용하여) 복합 서술문 안에 정의한 다음에 exit 함수를 호출해야 한다.

부모 프로세스는 자손이 끝나기를 기다려야 한다. 자손 프로세스는 끝내기 전에 부모 프로세스에게 신호를 보내 알려야 한다. 자손 프로세스가 끝나고 부모 프로세스가 그 신호를 받지 못하면 자손 프로세스가 여전히 보이게 되는데 이를 좀비 프로세스라고 부른다.

부모 프로세스가 자손이 끝나기를 기다려야 한다면 waitForChild 멤버를 호출할 수 있다. 이 멤버는 자손 프로세스의 종료 상태를 부모에게 돌려준다.

자손 프로세스는 계속 살아 있지만 부모는 죽는 경우가 있다. 이것은 자연스러운 사건이다. 부모는 자손 프로세스보다 먼저 죽는 경향이 있다. C++ 세계에서는 이것을 데몬 프로그램이라고 부른다. 데몬에서 부모 프로세스는 죽고 자손 프로그램은 기본 init 프로세스의 자손으로 실행을 계속한다. 다시, 자손이 마침내 죽을 때 신호가 그의 `의붓-부모' init에게 전송된다. 이것은 좀비를 생성하지 않는다. init이 자신의 모든 (의붓) 자손들의 종료 신호를 잡기 때문이다. 데몬 프로세스의 생성은 아주 간단하다. Fork 클래스만 있으면 된다 (24.2.4항).

24.2.3: 방향전환 심화연구

이전에 6.6.2항에서 스트림은 ios::rdbuf 멤버 함수를 사용하여 방향전환된다고 언급했다. 스트림의 streambuf를 또다른 스트림에 할당함으로써 두 스트림 객체 모두 같은 streambuf에 접근한다. 그리하여 C++ 언어 자체의 수준에서 방향전환을 구현한다.

이것은 C++ 프로그램의 문맥 안에서는 잘 작동한다. 그러나 이 문맥을 벗어나면 바로 방향전환은 종료한다. 운영 체제는 streambuf 객체를 알지 못한다. 이 상황은 프로그램이 system 호출을 사용하여 부프로그램을 기동시킬 때에도 만난다. 이 절의 끝에 있는 예제 프로그램은 C++ 방향전환을 사용하여 cout에 삽입되는 정보를 파일로 방향전환한 다음,

    system("echo hello world")
위와 같이 호출하여 눈에 익은 텍스트 한 줄을 화면에 출력한다. echo는 정보를 표준 출력에 쓰기 때문에 운영 체제가 C++의 방향전환을 인지한다면 이것이 바로 프로그램의 방향전환된 파일이 될 것이다.

그러나 방향전환은 일어나지 않는다. 대신에 hello world는 여전히 프로그램의 표준 출력에 나타나고 방향전환된 파일은 그대로이다. hello world를 방향전환된 파일에 쓰려면 방향전환을 운영 체제 수준에서 실현해야 한다. 이를 위하여 어떤 운영 체제는 (예를 들어 유닉스와 그 친구들은) dupdup2 같은 시스템 호출을 제공한다. 이런 시스템 호출의 사용 예제는 24.2.5항에 보여준다.

다음은 시스템 수준에서 실패하는 방향전환의 예이다. C++streambuf 방향전환을 사용한다.

    #include <iostream>
    #include <fstream>
    #include <cstdlib>
    using namespace std;

    int main()
    {
        ofstream of("outfile");

        streambuf *buf = cout.rdbuf(of.rdbuf());
        cout << "To the of stream\n";
        system("echo hello world");
        cout << "To the of stream\n";
        cout.rdbuf(buf);
    }
    /*
        `outfile' 파일에 출력함:

        To the of stream
        To the of stream

        표준 출력에 출력함:

        hello world
    */

24.2.4: `데몬' 프로그램

어플리케이션 안에서 fork의 유일한 목적은 자손 프로세스를 기동시키는 것이다. 자손 프로세스를 부화시키고 나면 부모 프로세스는 즉시 종료한다. 이런 일이 일어나더라도 자손 프로세스는 init 프로세스의 자손으로 실행을 계속한다. init 프로세스는 유닉스 시스템에서 언제나 제일 먼저 실행되는 첫 프로세스이다. 그런 프로세스를 배경 프로세스로 실행되는 데몬이라고 부른다.

다음 예제는 평범한 C 프로그램으로 어렵지 않게 구성할 수 있지만 C++ 주해서에 포함했다. 왜냐하면 현재 논의 중인 Fork 클래스와 밀접하게 관련이 있기 때문이다. daemon 멤버를 Fork 클래스에 추가할까도 생각했지만 결국 포기하기로 결정했다. 데몬 프로그램의 생성은 아주 간단하며 현재 Fork가 제공하는 특징들 외에 별다른 특징을 요구하지 않기 때문이다.

다음은 데몬 프로그램의 생성 방법을 시연하는 예제이다. 자손 프로세스는 exit 함수로 종료하지 않고 throw 0으로 예외를 던진다. 그러면 자손의 main 함수에서 catch 절이 잡는다. 이렇게 하면 자손 프로세스에 정의된 객체는 모두 적절하게 파괴된다는 보장이 있다.

    #include <iostream>
    #include <unistd.h>
    #include "fork.h"

    class Daemon: public Fork
    {
        virtual void parentProcess()        // 부모는 아무것도 하지 않는다.
        {}
        virtual void childProcess()         // 자손의 행위
        {
            sleep(3);
                                            // 그냥 메시지 출력...
            std::cout << "Hello from the child process\n";
            throw 0;                        // 자손 프로세스 종료
        }
    };

    int main()
    try
    {
        Daemon().fork();
    }
    catch(...)
    {}

    /*
        출력:
    다음 명령어를 기다린 다음, 3초 후에 출력된다.
    Hello from the child process
    */

24.2.5: `Pipe' 클래스

시스템 수준에서 방향전환은 pipe 시스템 호출이 생성하는 파일 기술자의 사용을 요구한다. 두 프로세스가 파일 기술자로 서로 통신하고 싶을 때 다음과 같은 일이 일어난다. 기본적으로 단순하지만 에러가 쉽게 끼어들 수 있다. (자손 또는 부모) 두 프로세스가 사용할 수 있는 파일 기술자의 함수들은 쉽게 뒤죽박죽될 수 있다. 기록 에러를 방지하기 위하여 기록은 한 번만 적절하게 설정하고 그 다음부터는 여기에서 개발한 Pipe같은 클래스 안에 감출 수 있다. 그 특징들을 한 번 살펴 보자 (pipedup 같은 함수들을 사용하려면 컴파일러는 먼저 <unistd.h> 헤더를 읽어야 한다): 이제 하나 이상의 Pipe 객체로 방향전환을 쉽게 구성할 수 있으므로 ForkPipe를 다양한 예제 프로그램에 사용해 보자.

24.2.6: 클래스 `ParentSlurp'

Fork로부터 파생된 ParentSlurp 클래스는 (/bin/ls처럼) 독립 프로그램을 실행하는 자손 프로세스를 기동시킨다. 화면에는 보이지 않지만 프로그램의 (표준) 출력은 부모 프로세스가 읽는다.

데모의 목적으로 부모 프로세스는 표준 출력 스트림으로부터 받은 줄 앞에다 번호를 붙여서 화면에 쓴다. 부모의 표준 입력 스트림을 방향전환하는 것은 매력적이다. std::cin 입력 스트림을 사용하여 부모는 자손 프로세스로부터 출력을 읽을 수 있기 때문이다. 그러므로 프로그램에서 단 하나의 파이프가 부모에게는 입력 파이프로 그리고 자손에게는 출력 파이프로 사용된다.

ParentSlurp 클래스는 다음의 특징이 있다.

다음 프로그램은 ParentSlurp 객체를 생성한다. 그리고 자신의 fork() 멤버를 호출한다. 프로그램이 시작된 디렉토리에 있는 파일이름에 줄번호를 붙여서 리스트를 출력한다. 프로그램은 또 fork.opipe.o 그리고 waitforchild.o 오브젝트 파일도 필요함에 주목하라 (이전 소스 참고):
    int main()
    {
        ParentSlurp().fork();
    }
    /*
        출력 (예제용일 뿐임, 실제 출력은 컴퓨터마다 다름):

        1: a.out
        2: bitand.h
        3: bitfunctional
        4: bitnot.h
        5: daemon.cc
        6: fdinseek.cc
        7: fdinseek.h
        ...
    */

24.2.7: 여러 자손과의 통신

다음으로 올라갈 계단은 자손-프로세스를 관제하는 것이다. 여기에서 부모 프로세스는 모든 자손 프로세스에 책임이 있다. 뿐만 아니라 표준 출력도 읽어야 한다. 사용자는 부모 프로세스의 표준 입력에 정보를 타자해 넣는다. 이를 위해 간단한 명령 언어가 사용된다. 자손 프로세스가 일정 시간 동안 텍스트를 받지 못하면 메시지를 부모 프로세스에게 보내 불만을 제기한다. 그 메시지들은 그대로 표준 출력 스트림에 복사되어 사용자에게 전송된다.

우리의 관제 프로그램에서 문제는 여러 소스로부터 비동기 입력이 허용된다는 것이다. 입력은 표준 입력은 물론이고 파이프의 입력 단에서도 일어날 수 있다. 또한 여러 출력 채널이 사용된다. 이와 같은 상황을 처리하기 위해 select 시스템 호출이 개발되었다.

24.2.7.1: `Selector' 클래스: 인터페이스

select 시스템 호출은 비동기 I/O 다중화(멀티플렉싱)을 처리하기 위하여 개발되었다. select 시스템 호출은 파일 기술자 집합에서 동시다발적으로 일어나는 입력을 처리한다.

select 함수는 좀 복잡하다. 이 함수를 완전하게 논의하는 것은 이 책의 범위를 벗어난다. selectclass Selector에 싸 넣음으로써 세부사항들을 감추고 직관적으로 단순한 인터페이스를 제공하면 사용법은 간단해진다. Selector 클래스는 다음의 특징이 있다.

24.2.7.2: 클래스 `Selector': 구현

Selector 클래스의 멤버 함수들에게 다음의 임무가 주어진다. 나머지 두 멤버는 지원 멤버이다. 비-멤버 함수가 사용하면 안된다. 그러므로 비밀 구역에 선언된다.

24.2.7.3: 클래스 `Monitor': 인터페이스

관제 프로그램은 Monitor 객체를 사용한다. 이 객체가 대부분의 작업을 맡는다. 자신의 일을 수행하기 위하여 Monitor 클래스의 공개 인터페이스는 기본 생성자와 run 멤버만 제공한다. 다른 모든 멤버 함수는 비밀 구역에 위치한다.

Monitor는 비밀 Commands 열거체를 정의한다. 이 열거체에는 입력 명령어가 지원하는 다양한 명령어들이 심볼로 나열되어 있다. 뿐만 아니라 여러 데이터 멤버도 정의한다. 데이터 멤버 중에서 Selector 객체와 map 객체는 자손 순서 번호를 키로 사용하고 Child 객체를 가리키는 포인터를 값으로 사용한다 (24.2.7.7목). 그리고 Monitor는 정적 배열 s_handler[] 멤버가 있다. 이 멤버는 사용자 명령어를 처리하는 멤버 함수를 포인터로 저장한다.

소멸자도 구현해야 한다. 그러나 그 구현은 독자 여러분에게 연습 문제로 남긴다. 다음은 Monitor의 인터페이스이다. 함수객체를 생성하는 Find 내포 클래스의 인터페이스도 제시한다.

    class Monitor
    {
        enum Commands
        {
            UNKNOWN,
            START,
            EXIT,
            STOP,
            TEXT,
            sizeofCommands
        };

        typedef std::map<int, std::shared_ptr<Child>> MapIntChild;

        friend class Find;
        class Find
        {
            int     d_nr;
            public:
                Find(int nr);
                bool operator()(MapIntChild::value_type &vt) const;
        };

        Selector    d_selector;
        int         d_nr;
        MapIntChild d_child;

        static void (Monitor::*s_handler[])(int, std::string const &);
        static int s_initialize;

        public:
            enum Done
            {};

            Monitor();
            void run();

        private:
            static void killChild(MapIntChild::value_type it);
            static int initialize();

            Commands    next(int *value, std::string *line);
            void    processInput();
            void    processChild(int fd);

            void    createNewChild(int, std::string const &);
            void    exiting(int = 0, std::string const &msg = std::string());
            void    sendChild(int value, std::string const &line);
            void    stopChild(int value, std::string const &);
            void    unknown(int, std::string const &);
    };

클래스 유형이 아닌 데이터 멤버가 하나만 있기 때문에 클래스의 생성자는 인라인으로 구현해도 되는 아주 간단한 함수이다.

    inline Monitor::Monitor()
    :
        d_nr(0)
    {}

24.2.7.4: `Monitor' 클래스: s_handler 배열

s_handler 배열은 함수를 가리키는 포인터들을 담고 있는데 역시 초기화할 필요가 있다. 여러가지 방식으로 달성할 수 있다.

24.2.7.5: `Monitor' 클래스: `run' 멤버

Monitor 객체의 핵심 조치는 run 멤버가 수행한다. 다음은 run의 구현과 s_initialize의 정의이다.
    #include "monitor.ih"

    int Monitor::s_initialize = Monitor::initialize();

    void Monitor::run()
    {
        d_selector.addReadFd(STDIN_FILENO);

        while (true)
        {
            cout << "? " << flush;
            try
            {
                d_selector.wait();

                int fd;
                while ((fd = d_selector.readFd()) != -1)
                {
                    if (fd == STDIN_FILENO)
                        processInput();
                    else
                        processChild(fd);
                }
                cout << "NEXT ...\n";
            }
            catch (char const *msg)
            {
                exiting(1, msg);
            }
        }
    }

processInput 멤버 함수는 프로그램의 표준 입력 스트림으로 사용자가 입력한 명령어를 읽는다. 멤버 자체는 단순하다. next 멤버를 호출하여 사용자가 입력한, 다음 명령어를 얻는다. 그 다음에 s_handler[] 배열에서 해당되는 원소에 상응하는 함수를 호출한다. 다음은 processInput 멤버와 next 멤버이다.

    void Monitor::processInput()
    {
        string line;
        int    value;
        Commands cmd = next(&value, &line);
        (this->*s_handler[cmd])(value, line);
    }

    Monitor::Commands Monitor::next(int *value, string *line)
    {
        if (!getline(cin, *line))
            exiting(1, "Monitor::next(): reading cin failed");

        if (*line == "start")
            return START;

        if (*line == "exit" || *line == "quit")
        {
            *value = 0;
            return EXIT;
        }

        if (line->find("stop") == 0)
        {
            istringstream istr(line->substr(4));
            istr >> *value;
            return !istr ? UNKNOWN : STOP;
        }

        istringstream istr(line->c_str());
        istr >> *value;
        if (istr)
        {
            getline(istr, *line);
            return TEXT;
        }

        return UNKNOWN;
    }

d_select가 감지한 다른 모든 입력은 자손 프로세스가 생성한다. d_selectreadFd 멤버는 상응하는 입력 파일 기술자를 돌려주므로 이 기술자를 processChild에 건넬 수 있다. IFdStreambuf로 그의 정보를 입력 스트림으로부터 읽는다 (24.1.2.1목). 여기에 사용된 통신 프로토콜은 단순하다. 줄이 입력될 때마다 자손은 정확하게 한 줄의 텍스트를 되돌려 줌으로써 응답한다. 그러면 이 줄을 processChild가 읽는다.

    void Monitor::processChild(int fd)
    {
        IFdStreambuf ifdbuf(fd);
        istream istr(&ifdbuf);
        string line;

        getline(istr, line);
        cout << d_child[fd]->pid() << ": " << line << '\n';
    }

위의 소스에 사용된 d_child[fd]->pid()의 생성 방법은 특별히 주의를 기울일 가치가 있다. Monitormap<int, shared_ptr<Child>> d_child 데이터 멤버를 정의한다. 이 멤버에는 자손의 순서 번호가 키로 들어 있고 Child 객체를 가리키는 (공유) 포인터가 값으로 들어 있다. 여기에서는 Child 객체가 아니라 공유 포인터를 사용한다. 맵이 제공하는 편의기능들을 사용하고는 싶지만 Child 객체를 되풀이해서 복사하고 싶지는 않기 때문이다.

24.2.7.6: `Monitor' 클래스: 예제

run의 구현을 다루어 보았으므로 사용자가 입력할 가능성이 있는 다양한 명령어에 집중해 보겠다. 프로그램의 main 함수는 단순하고 주석도 따로 더 필요없다.
    int main()
    try
    {
        Monitor().run();
    }
    catch (int exitValue)
    {
        return exitValue;
    }

24.2.7.7: `Child' 클래스

Monitor 객체가 자손 프로세스를 기동시킬 때 Child 클래스의 객체를 생성한다. Child 클래스는 Fork 클래스로부터 파생되므로 (이전 절에서 논의한 바와 같이) 데몬처럼 작동할 수 있다. Child는 데몬 클래스이므로 그의 부모 프로세스는 빈 함수로 구현되어 있을 것임에 틀림없다. childProcess 멤버는 구현이 비어 있지 않다. 다음은 Child 클래스의 특징이다.

24.3: 비트 연산을 수행하는 함수객체

미리 정의된 여러 함수객체를 18.1절에 소개했다. 미리 정의된 객체들은 단항 연산자와 이항 연산자의 항의 갯수에 상응하여 산술 연산과 관계 연산 그리고 논리 연산을 수행한다.

빠진 연산자들이 있는 듯하다. 비트 연산에 상응하는 함수객체들은 미리 정의되어 있지 않은 것 같다. 그렇지만 미리 정의된 객체들이 있으므로 별 어려움 없이 구현할 수 있다. 다음 예제는 비트 연산자를 (operator&) 호출하는 함수객체를 구현한 클래스 템플릿을 보여주고 단항 부인 연산자를 (operator~) 호출하는 함수객체를 구현한 클래스 템플릿을 보여준다. 비슷한 함수객체를 다른 연산자에 구현하는 것은 독자 여러분에게 숙제로 남긴다.

다음은 비트 operator& 연산자를 호출하는 함수객체의 구현이다.

    #include <functional>

    template <typename _Tp>
    struct bitAnd: public std::binary_function<_Tp, _Tp, _Tp>
    {
        _Tp &&operator()(_Tp const &__x, _Tp const &__y) const
        {
            return std::move(__x & __y);
        }
    };

다음은 단항 부인 연산자 operator~()를 호출하는 함수객체의 구현이다.

    #include <functional>

    template <typename _Tp>
    struct bit_not: public std::unary_function<_Tp, _Tp>
    {
        _Tp &&operator()(_Tp const &__x) const
        {
            return _Tp(~__x);
        }
    };

미리 정의된 함수객체에서 이리 저리 빠진 객체들은 bitfunctional 파일에 구현되어 있다. 이 파일은 cplusplus.yo.zip 압축 파일에 있다. 이 클래스들은 기존의 클래스 템플릿으로부터 파생된다 (예를 들어 std::binary_function 그리고 std::unary_function). 이 바탕 클래스들은 STL에 정의된 다양한 총칭 알고리즘이 기대하고 (사용하는) 여러 유형을 정의한다 (제 19장). 그래서 C++ 헤더 파일 bits/stl_function.h에 제시된 조언을 따른다.

   *  표준 함수객체는 unary_function 구조체와 binary_function 구조체로부터 파생된다.
   *  이 두 클래스에는 총칭 (템플릿) 프로그래밍을 돕기 위한
   *  유형정의(typedef) 외에는 아무것도 들어 있지 않다. 여러분 만의 함수객체를
   *  구현하려면 똑같은 원칙을 지켜야 할 것이다.

다음은 bit_and를 사용하는 예제이다. int 값으로 구성된 벡터로부터 홀수를 모두 제거한다.

    #include <iostream>
    #include <algorithm>
    #include <vector>
    #include <iterator>
    #include "bitand.h"
    using namespace std;

    int main()
    {
        vector<int> vi;

        for (int idx = 0; idx < 10; ++idx)
            vi.push_back(idx);

        copy
        (
            vi.begin(),
            remove_if(vi.begin(), vi.end(), bind2nd(bitAnd<int>(), 1)),
            ostream_iterator<int>(cout, " ")
        );
        cout << '\n';
    }
    /*
        출력:

        0 2 4 6 8
    */

24.4: 이항 연산자를 클래스에 추가하기

11.6절에서 보았듯이 const & 인자를 기대하는 이항 연산자는 이동-인지 클래스에 구현할 수 있다. 이동-인지 이항 연산자를 사용하고 첫 인자에 rvalue 참조를 사용하면 된다. 이어서 이 함수는 이항 할당 멤버를 사용하여 구현할 수 있다. 다음 예제는 지어낸 Binary 클래스에 대하여 이 접근법을 보여준다.
    class Binary
    {
        public:
            Binary();
            Binary(int value);
                // 복사 생성자와 이동 생성자는 기본으로 사용할 수 있다. 
                // 아니면 명시적으로 선언하고 구현하면 된다.

            Binary &operator+=(Binary const &other);    // 본문 참조
    };

    inline Binary &&operator+(Binary &&lhs, Binary const &rhs)
    {
        return std::move(lhs += rhs);   // 복사/이동 생성을 회피한다.
    }

    inline Binary &&operator+(Binary const &lhs, Binary const &rhs)
    {
        Binary tmp(lhs);
        return std::move(tmp) + rhs;
    }

그러므로 이항 연산자의 구현은 결국 이항 할당 연산자를 사용할 수 있는가에 달려 있다.

템플릿 함수는 실제로 사용되기 전에는 구체화되지 않기 때문에 절대로 구체화되지 않는 비-존재 함수를 템플릿에 언급할 수 있다. 그런 함수가 실제로 호출되면 빠진 함수 때문에 컴파일러는 에러 메시지로 응답할 것이다.

이렇게 하면 이동 가능한 이항 연산자와 이동 불가능한 이항 연산자를 모두 템플릿으로 구현할 수 있다. 그러면 짝이 되는 이항 할당이 존재할 경우에만 이항 연산자를 호출할 수 있다. 위의 덧셈 이항 연산자를 구현한 템플릿 함수는 모습이 다음과 같다.

    #ifndef INCLUDED_BINOPS_H_
    #define INCLUDED_BINOPS_H_

    #include <utility>

    template <typename Type>
    Type operator+(Type &&lhs, Type const &rhs)
    {
        return lhs += rhs;
    }

    template <typename Type>
    Type operator+(Type const &lhs, Type const &rhs)
    {
        Type tmp(lhs);
        return operator+(std::move(tmp), rhs);
    }

    #endif

단점이 있다. 이런 함수 템플릿을 정의할 때 꼭 확인하자. rvalue 참조를 첫 매개변수로 지정한 이항 연산자는 상수 lrvalue 참조를 첫 매개변수로 지정한 이항 연산자보다 먼저 정의해야 한다. 그렇지 않으면 이 템플릿을 사용하는 프로그램은 무한 재귀에 빠져 실패한다.

다른 이항 연산자에 대한 함수 템플릿은 이런 덧셈 연산자에 쉽게 추가할 수 있다. binops.h 파일에 모두 모은 후에 이 파일을 클래스 헤더 파일에 포함시켜 이항 연산자들을 추가하면 된다.

흥미롭게도 이동 생성자를 구현하지 않은 클래스도 이 템플릿을 사용할 수 있다. 이동 생성자 자체는 이항 연산자 구현에서 절대로 호출되지 않기 때문이다 (그의 호출은 복사 생략으로 최적화된다). 다음 프로그램은 (출력 서술문이 담긴 수정된 함수 템플릿을 사용하므로) 이동 생성자가 정의되어 있든 없든 똑같이 행위한다.

#include <iostream>
using namespace std;

template <typename Class>
Class &&operator+(Class &&lhs, Class const &rhs)
{
    cout << "operator+(Class &&lhs, Class const &rhs)\n";

    return std::move(lhs += rhs);
}

template <typename Class>
Class &&operator+(Class const &lhs, Class const &rhs)
{
    cout << "operator+(Class const &, Class const &)\n";

    Class tmp(lhs);
    return operator+(std::move(tmp), rhs);
}

template <typename Class>
Class &&operator-(Class &&lhs, Class const &rhs)
{
    return std::move(lhs -= rhs);
}

template <typename Class>
Class &&operator-(Class const &lhs, Class const &rhs)
{
    Class tmp(lhs);
    return operator-(std::move(tmp), rhs);
}

class Class
{
    public:
        Class() = default;
        Class(Class const &other) = default;
        Class(int)
        {}
        Class(Class &&tmp)
        {
            cout << "Move constructor\n";
        }

        Class &operator=(Class const &rhs) = default;

        Class &operator+=(Class const &rhs)
        {
            cout << "operator+=\n";
            return *this;
        }
};

Class factory()
{
    return Class{};
}

int main()
{
    Class lhs;
    Class rhs;
    Class result;

    result = lhs + rhs;
    result = factory() + rhs;

//    result = lhs - rhs;   // 이것은 컴파일되지 않는다.
                            // operator-=가 아직 정의되지 않았기 때문이다.
}

24.4.1: 승격을 허용하는 이항 연산자

이전 절에 소개한 함수 템플릿은 승격을 허용하지 않는다. 예를 들어 다음과 같은 서술문은
  result = rhs + 2;
컴파일러 에러를 일으킨다. 템플릿 인자 추론 알고리즘이 승격을 인지하지 못하기 때문이다. 위의 서술문을 컴파일러가 받아들이도록 재작성할 필요가 있다.
  result = rhs + Class(2);

승격이 바람직하다면 어떻게 산술 연산자 함수 템플릿을 바꾸어서 승격하도록 만들 수 있을까? 승격이라면 연산자 함수의 인자는 유형에 상관이 없다. 적어도 그 중에 하나는 적절한 반영 할당 연산자를 제공하는 클래스 유형이어야 한다. 그러나 함수 템플릿을 설계할 때 두 피연산자 중 어느 연산자가 클래스 유형인지 알 수 없다. 그래서 연산자 함수의 두 매개변수에 두 개의 템플릿 유형의 매개변수를 지정할 필요가 있다. 그러므로 함수 템플릿은 다음과 같이 시작해야 한다.

    template <typename LHS, typename RHS>
    ReturnType operator+(LHS const &lhs, RHS const &rhs)
이 시점에서 아직은 ReturnType을 지정할 수 없다. RHS를 승격할 수 있거나 LHS와 같다면 LHS가 될 것이고 LHSRHS로 승격된다면 RHS가 될 것이기 때문이다.

RHSLHS로 승격할 수 있는지 알아 보기 위해 이제 간단하게 LpromotesR 템플릿 메타 프로그래밍 클래스를 설계한다. 이 템플릿은 두 개의 템플릿 유형 매개변수를 사용한다. 두 번째 (오른쪽) 유형을 첫 번째 (왼쪽) 유형으로 승격할 수 있으면 값을 true(1)로 정의하고 그렇지 않으면 값을 false(0)로 정의한다. 이전 유형 변환의 가능성에서 보았던 규칙을 적용한다 (23.8.3항).

    template <typename L, typename R>
    class LpromotesR
    {
        struct Char2
        {
            char array[2];
        };
        static R const &makeR();
        static char test(L const &);
        static Char2 test(...);

        public:
            LpromotesR() = delete;
            enum { yes = sizeof(test(makeR())) == sizeof(char) };
    };
LpromotesR 클래스에서 RL로 승격할 수 있으면 test(L const &) 함수가 선택되고 그렇지 않으면 test(...) 가 선택된다. 이 두 test 함수의 반환 유형은 크기가 다르기 때문에 컴파일러는 enumyes에 1 또는 0을 할당할 수 있다. 생성자에 R 유형이 explicit 키워드로 명시적으로 선언되어 있으면 yes의 값은 0이다. 그리고 LR이 어쩌다가 유형이 같으면 1이다.

이제 유형을 다른 유형으로 승격할 수 있는지 없는지 결정할 수 있으므로 LHS 또는 RHS를 함수 템플릿의 반환 유형으로 선택할 수 있다. RHSLHS로 승격할 수 있으면 LHS를 함수 템플릿의 반환 유형으로 사용하라. 그렇지 않으면 RHS를 사용하라.

물론 세 번째 가능성이 있다. LHSRHS 유형은 서로 상대방의 생성자가 사용할 수 없다. 그 경우, 또다른 생성자가 어디엔가 자리하고 있어서 그 상황을 처리해 주지 않는 한, 컴파일러는 다음과 같은 에러 메시지를 보여준다.

    no match for 'operator+' in '...'

승격 가능한 유형으로 되돌아가자. 이제 LpromotesR을 사용하면 어느 유형을 승격할 수 있는지 결정할 수 있다. 돌려주는 값을 이전에 소개한 IfElse 메타 프로그래밍 클래스 템플릿에서 선택자로 사용할 수 있다 (23.2.2.2목).

이제 LHS 또는 RHS를 연산자 템플릿 함수의 반환 유형으로 선택할 수 있으므로 승격을 지원하는 산술 연산자 함수 템플릿을 생성할 수 있다.

    template <typename LHS, typename RHS>
        typename FBB::IfElse<FBB::LpromotesR<LHS, RHS>::yes, LHS, RHS>::type
        operator<<(LHS const &lhs, RHS const &rhs)
    {
        typedef typename FBB::IfElse<
                                FBB::LpromotesR< LHS, RHS >::yes, LHS, RHS
                            >::type Type;
        Type tmp(lhs);
        return std::move(tmp) << type(rhs);
    }
함수의 반환 유형은 IfElsetype이다. RHSLHS로 승격할 수 있으면 LHS로 선택되고 아니면 RHS가 선택된다. 같은 트릭이 함수 몸체에서 사용되어 tmp의 유형을 결정한다.

이제 승격을 할 수 있다. rvalue 참조 매개변수를 정의한 함수 템플릿은 그대로이다. 이 모든 것을 이용하여 컴파일러는 다음 결정을 내릴 수 있다 (Class를 의도된 클래스 이름으로 사용하고 TypeClass로 승격할 수 있는 유형처럼 사용하며 그리고 @를 사용된 연산자의 총칭 표식으로 사용한다.). 그렇지 않고 따로 지정하지 않으면 (LHS const &lhs, RHS const &rhs) 매개변수 리스트를 정의한 함수 템플릿이 사용된다.

    Class obj;
    Type value;

    obj @ obj           // 승격 없음
    obj @ Class()       // 위와 같음

    obj @ value;        // value는 Class로 승격됨
    Class() @ value;    // 위와 같음

    value @ obj;        // 위와 같음
    value @ Class();    // 위와 같음

    Class() @ obj;      // operator@(Class &&, Class const &)를 호출함
    Class() @ Class();  // 위와 같음

24.d: 클래스에 이항 연산자 추가하기(심화)

11.6절에서 보았듯이 const & 인자를 기대하는 이항 연산자는 그 연산을 구현한 멤버를 사용하여 구현할 수 있다. 기본 예외만 보장한다. 이 번에는 이항 할당 멤버를 사용하여 구현할 수 있다. 다음 예제는 Binary 클래스에 대하여 이 접근법을 보여준다.
    class Binary
    {
        public:
            Binary();
            Binary(int value);
                // 기본으로 복사 생성자와 이동 생성자가 있다. 아니면,
                // 명시적으로 선언하고 구현하면 된다.

            Binary &operator+=(Binary const &other) &;    // 본문 참조
            Binary &&operator+=(Binary const &other) &&;

        private:
            void add(Binary const &rhs);

        friend Binary operator+(Binary const &lhs, Binary const &rhs);
        friend Binary operator+(Binary &&lhs, Binary const &rhs);
    };

결국 이항 연산의 구현은 기본 이항 연산을 구현하고 자신을 호출한 객체를 변경하는 멤버가 (즉, 예제에서는 void Binary::add(Binary const &) 멤버가) 있는가 없는가에 달려 있다.

템플릿 함수는 실제로 사용되기 전에는 구체화되지 않으므로 절대로 구체화되지 않는 템플릿 함수로부터 존재하지 않는 함수를 호출할 수 있다. 그런 템플릿 함수가 절대로 구체화되지 않는다면, 아무 일도 일어나지 않는다. (우발적으로) 구체화되더라도 컴파일러가 에러 메시지를 보여주고 함수가 없다고 불평할 것이다.

이렇게 하면 이동 가능하든 불가능하든 모든 이항 연산자를 템플릿으로 구현할 수 있다. 다음 항에서 이항 연산자를 제공하는 Binops 클래스 템플릿을 개발한다. Derived 클래스의 완벽한 구현은 C++ 주해서의 소스 저장소 annotations/yo/concrete/examples/binopclasses.cc 파일에 있다. 삽입 연산자와 추출 연산자를 어떻게 클래스에 추가하는지 보여준다.

24.d.a: 연산자만 사용하기

11.6절에서 덧셈 연산자는 add 멤버의 관점에서 구현되었다. 이 방법은 함수 템플릿을 개발할 때는 별로 매력적이지 않다. add 함수는 비밀 멤버이고 모든 함수 템플릿에 친구로 선언해야만 그 함수에 접근할 수 있기 때문이다.

11.6절의 말미에서 add의 구현이 operator+=(Class const &rhs) &&으로 제공되는 것을 보았다. 이 연산자는 그 시점부터 나머지 덧셈 연산자를 구현한다.

    inline Binary &operator+=(Binary const &rhs) &
    {
        return *this = Binary{*this} += rhs;        
    }

    Binary operator+(Binary &&lhs, Binary const &rhs)
    {
        return std::move(lhs) += rhs;
    }

    Binary operator+(Binary const &lhs, Binary const &rhs)
    {
        return Binary{lhs} += rhs;
    }

이 구현에서 add 멤버는 더 이상 필요하지 않다. 평범한 이항 연산자는 자유 함수이다. 그러므로 손쉽게 함수 템플릿으로 변환할 수 있을 것이다. 예를 들어,

    template <typename Binary>
    Binary operator+(Binary const &lhs, Binary const &rhs)
    {
        return Binary{lhs} += rhs;
    }

24.d.a.a: 이름공간을 줄 것인가 말 것인가?

그렇지만 함수 템플릿 Binary operator+(Binary const &lhs, Binary const &rhs)를 사용할 때 예상하지 못한 미묘한 문제를 만날 가능성이 있다. 다음 프로그램을 연구해 보자. 실행하면 1이 아니라 12를 화면에 출력한다.
    enum Values
    {
        ZERO,
        ONE
    };
    
    template <typename Tp>
    Tp operator+(Tp const &lhs, Tp const &rhs)
    {
        return static_cast<Tp>(12);
    };
    
    int main()
    {
        cout << (ZERO + ONE);       // 12를 보여준다.
    }
이 복잡성을 피하려면 연산자를 각자의 이름공간에 정의하면 된다. 그러나 그러면 그 이항 연산자를 사용하는 모든 클래스도 그 이름공간에 정의할 필요가 있다. 이런 제한은 별로 달갑지 않다. 다행스럽게도 더 좋은 대안이 있다. CRTP를 사용하면 된다 (22.12절).

24.d.b: CRTP 그리고 연산자 함수 템플릿 정의하기

Binops 클래스 템플릿으로부터 클래스를 파생시킬 때 CRTP(Curiously Recurring Template Pattern)를 사용하여 Binops<Derived> 클래스의 인자들에 대하여 연산자를 정의한다. 파생 클래스를 자신의 템플릿 인자로 받는 바탕 클래스를 정의한다.

그리하여 Binops 클래스와 더불어 Binops<Derived> 유형의 인자를 기대하는 연산자들도 추가로 함께 정의한다.

    template <class Derived>
    struct Binops
    {
        Derived &operator+=(Derived const &rhs) &;
    };

    template <typename Derived>
    Derived operator+(Binops<Derived> const &lhs, Derived const &rhs)
    {
        return Derived{static_cast<Derived> const &>(lhs) } += rhs;
    }
    // Binops<Derived> &&lhs도 비슷하게 구현

이런 식으로 클래스가 Binops로부터 파생되고 rvalue 참조 객체에 묶인 operator+= 멤버를 지원하면 갑자기 다른 모든 이항 덧셈 연산자들이 제공된다.

    class Derived: public Binops<Derived>
    {
        ...
        public:
            ...
            Derived &&operator+=(Derived const &rhs) &&
    };
거의 모두 지원되지만, 하나만은 예외인데....

사용이 불가능한 연산자는 바로 lvalue 참조에 묶인 반영 덧셈 연산자이다. 그의 함수 이름이 Derived 클래스에 있는 함수 이름과 동일하기 때문에 사용자 수준에서는 보이지 않는다.

Derived 클래스에 using Binops<Derived>::operator+=를 선언하면 이 문제를 쉽게 해결할 수 있지만 별로 마음에 드는 해결책은 아니다. Derived 클래스에 구현된 이항 연산자마다 따로따로 using 선언을 제공할 필요가 있기 때문이다.

그러나 훨씬 더 바람직한 해결책이 있다. 숨겨진 바탕 클래스 연산자를 완벽하게 피하는 지극히 아름다운 방법이 비베-마틴 윈지아(Wiebe-Marten Wijnja)에 의하여 제안되었다. 그의 추측에 의하면 operator+=를 lvalue 참조에 묶는다면 자유 함수로 문제없이 잘 정의할 수 있을 것이다. 그 경우 상속이 사용되지 않는다. 그러므로 함수가 보이지 않는 일은 일어나지 않는다. 결론적으로 using 지시어를 사용하지 않아도 된다.

다음과 같이 operator+= 자유 함수를 구현한다.

    template <class Derived>
    Derived &operator+=(Binops<Derived> &lhs, Derived const &rhs) 
    {
        Derived tmp{ Derived{ static_cast<Derived &>(lhs) } += rhs };
        tmp.swap(static_cast<Derived &>(lhs));
        return static_cast<Derived &>(lhs);
    }

오른쪽 피연산자가 꼭 Derived 클래스의 실체일 필요는 없다는 사실을 깨닫으면 이 디자인은 훨씬 더 유연해질 수 있다. operator<<를 연구해 보자. 비트별로 쉬프트하는 경우가 많다. size_t를 사용하여 쉬프트할 비트의 갯수를 지정한다. 실제로 두 번째 템플릿 유형 매개변수를 정의하면 오른쪽 피연산자의 유형을 완전히 일반화할 수 있다. 이 매개변수는 오른쪽 피연산자의 유형을 지정한다. operator+=의 인자 유형을 지정하는 것은 Derived 클래스에 달려 있다 (또는 다른 모든 이항 반영 연산자). 그러면 컴파일러는 나머지 이항 연산자들에 대하여 오른쪽 피연산자들의 유형을 추론할 것이다. 다음은 operator+= 자유 함수의 최종 구현이다.

    template <class Derived, typename Rhs>
    Derived &operator+=(Binops<Derived> &lhs, Rhs const &rhs) 
    {
        Derived tmp{ Derived{ static_cast<Derived &>(lhs) } += rhs };
        tmp.swap(static_cast<Derived &>(lhs));
        return static_cast<Derived &>(lhs);
    }

24.d.c: 삽입과 추출

클래스는 중복정의 삽입 연산자와 중복정의 추출 연산자를 자주 정의한다. 이 연산자들을 중복정의할 때 `반영 삽입 연산자'가 없기 때문에 지금까지 보여준 설계는 사용할 수 없다. 대신에 표준 멤버 함수의 서명을 추천한다. void insert(std::ostream &out) const로 객체를 ostream에 삽입하고 void extract(std::istream &in) const로 객체를 istream으로부터 추출할 수 있다. 이 함수들은 각각 삽입 연산자와 추출 연산자에서만 사용되므로 Derived 클래스의 비밀 인터페이스에 선언할 수 있다. 삽입 연산자와 추출 연산자를 Derived 클래스의 친구로 선언하는 대신에 friend Binops<Derived>만 지정한다. 이렇게 하면 Binops<Derived>로 비밀 구역에 인라인으로 iWrap 멤버와 eWrap 멤버를 정의할 수 있다. 각각 Derivedinsert 멤버와 extract 멤버를 호출하는 일만 한다.
    template <typename Derived>
    inline void Binops<Derived>::iWrap(std::ostream &out) const
    {
        static_cast<Derived const &>(*this).insert(out);
    }
그 다음 Binops<Derived>는 삽입 연산자와 추출 연산자를 친구로 선언한다. 그러면 이 연산자들은 각각 iWrapeWrap을 호출할 수 있다. Derived 클래스를 설계하는 프로그래머는 friend Binops<Derived>를 선언하기만 하면 된다. 다음은 중복정의 삽입 연산자의 구현이다.
    template <typename Derived>
    std::ostream &operator<<(std::ostream &out, Binops<Derived> const &obj)
    {
        obj.iWrap(out);
        return out;
    }

이 정도면 Binops 클래스 템플릿의 핵심 기능을 총 망라한 셈이다. 잠재적으로 Binops로부터 파생된 모든 클래스에 이항 연산자와 삽입/추출 연산자를 제공한다. 마지막으로 이 항의 서두에 지적했듯이 삽입 연산자와 추출 연산자를 제공하는 클래스를 완벽하게 구현한 것은 C++ 주해서의 소스 저장소에 있는 annotations/yo/concrete/examples/binopclasses.cc 파일에 있다.

24.5: 범위-기반의 for-회돌이와 포인터-범위

표준적인 범위-기반의 for-회돌이는 범위 지정에 배열이나 초기화 리스트 또는 (beginend 멤버를 통하여) 컨테이너가 제공하는 반복자 범위를 요구한다.

포인터 쌍으로 정의된 범위나 반복자 표현식으로 정의된 부범위는 현재 범위-기반의 for-회돌이와 조합하여 사용할 수 없다.

이 절에서 개발할 Ranger 클래스 템플릿은 범위-기반의 for-회돌이와 사용할 수 있는 범위를 정의한다. Ranger는 범위-기반의 응용 범위를 확장한다. 포인터 쌍, 최초의 포인터나 반복자 그리고 포인터 갯수, 또는 한 쌍의 반복자를 범위-기반의 for-회돌이에서 사용할 수 있는 범위로 변환한다. Ranger 클래스 템플릿은 한 쌍의 역방향 반복자를 처리하는 데에도 사용할 수 있다. 이는 범위-기반의 for-회돌이에서 지정하지 못한다.

Ranger 클래스 템플릿은 템플릿 유형 매개변수를 하나만 요구한다. Iterator가 그것으로서 역참조할 때 데이터에 도달하는 포인터 유형 또는 반복자를 나타낸다. 실제 어플리케이션에서는 Ranger의 템플릿 유형을 지정할 필요가 없다. ranger 함수 템플릿은 필요한 Iterator 유형을 추론하여 적절한 Ranger 객체를 돌려준다.

ranger 함수 템플릿은 다양한 방식으로 사용할 수 있다.

Ranger 클래스 템플릿 자체는 두 개의 Iterator const & 매개변수를 기대하는 생성자를 제공한다. 여기에서 IteratorRanger의 템플릿 유형 매개변수이다. 이름붙은 'Iterator'이지만 어떤 데이터 유형을 가리키는 포인터일 수도 있다 (예를 들어 std::string *).

이 클래스는 beginend 두 개의 멤버만 요구한다. 범위-기반의 for-회돌이에서 호출하는 유일한 멤버들이기 때문이다. 이 멤버들은 Iterator const 참조를 돌려주는 const 멤버일 수 있다. Iterator 자체가 포인터 유형이라면 (int *와 같이) 이것이 필수적인 반환 유형이다. `Iterator const &'라고 해서 역참조된 Iterator가 변경 불가능하다는 뜻은 아니기 때문에 begin()이 반환한 반복자가 가리키는 데이터가 실제로는 변경이 될 가능성이 있다. Iterator라고 할지라도 IteratorType const *이 아니거나 const_iterator 유형이 아니라면 변경될 수 있다.

역방향 반복자를 Ranger의 생성자에 건네면 (역방향 시작 반복자는 Ranger 생성자의 첫 인자로 건네고 역방향 끝 반복자는 두 번째 인자로 건네야 한다) Rangerbeginend 멤버는 역방향 반복자를 돌려준다. Ranger 객체를 사용하는 의도는 범위-기반의 for-회돌이에 대하여 범위를 정의하는 것이기 때문에 rbeginrend같은 멤버는 Ranger의 인터페이스에서 빼버렸다.

다음은 Ranger의 (인-클래스) 구현이다.

    template <typename Iter>
    class Ranger
    {
        Iter d_begin;
        Iter d_end;

        public:
            Ranger(Iter const &begin, Iter const &end)
            :
                d_begin(begin),
                d_end(end)
            {}

            Iter const &begin() const
            {
                return d_begin;
            }

            Iter const &end() const
            {
                return d_end;
            }
    };
ranger를 사용하는 것은 쉽다. 다음은 범위 기반의 for-회돌이로 프로그램의 명령줄 인자를 보여주는 예이다.
    // 필요한 모든 선언은 여기에 삽입한다.

    int main(int argc, char **argv)
    {
        for (auto ptr: ranger(argv, argc))
            cout << ptr << '\n';
    }

24.6: operator[]()에서 lvalue와 rvalue를 구별하기

operator[]의 문제는 lvalue로 사용되는지 rvalue로 사용되는지 구분하지 못한다는 것이다. 그래서 다음과 같은 오해에 익숙하다.
    Type const &operator[](size_t index) const
이것은 (객체가 변경되지 않기 때문에) rvalue로 사용된다고 생각한다.
    Type &operator[](size_t index)
이것은 (반환된 값을 변경할 수 있기 때문에) lvalue로 사용된다고 생각한다.

그러나 컴파일러는 operator[]가 호출된 객체의 const-상태로만 두 연산자 사이를 구분한다. const 객체라면 앞의 연산자가 호출되고 비-const 객체라면 언제나 뒤의 연산자가 사용된다. lvalue로 사용되든 rvalue로 사용되든 그에 상관이 없다.

lvalue와 rvalue 사이를 구분할 수 있으면 아주 유용하다. operator[]를 지원하는 클래스가 아주 복사하기 어려운 유형의 데이터를 저장하고 있는 상황을 생각해 보자. 그런 데이터라면 불필요한 복사를 방지하기 위하여 아마도 (예를 들어 shared_ptr을 사용한) 참조 횟수 세기가 사용될 것이다.

operator[]이 rvalue로 사용되는 한 그 데이터를 복사할 필요가 없다. 그러나 lvalue로 사용된다면 반드시 그 정보를 복사해야 한다.

프록시 디자인 패턴을 (cf. Gamma et al. (1995)) 사용하면 lvalue와 rvalue 사이를 구별할 수 있다. 프록시 디자인 패턴에서는 `실제 대상'을 대신하여 행위하기 위하여 또다른 클래스의 객체가 사용된다. 프록시(Proxy) 클래스는 데이터 자체가 제공할 수 없는 편의기능을 제공한다. 예를 들어 lvalue로 사용되는지 rvalue로 사용되는지 구별할 수 있다. 프록시 클래스는 실제 데이터에 직접적으로 접근할 수 없거나 또는 그러면 안되는 상황에 사용할 수 있다. 이런 관점에서 반복자 유형은 프록시 클래스의 예이다. 실제 데이터와 그 데이터를 사용하는 소프트웨어 사이에 한 겹의 층이 되어 주기 때문이다. 프록시 클래스는 포인터로 데이터를 저장한 클래스에서 포인터를 역참조할 수도 있다.

이 절은 operator[]가 lvalue로 사용되는지 아니면 rvalue로 사용되는지 구별하는 데 집중한다. 파일로부터 줄을 읽어 저장하는 Lines라는 클래스가 있다고 가정하자. 그의 생성자는 줄을 읽어 들일 스트림의 이름을 기대한다. 그리고 lvalue 또는 rvalue로 사용할 수 있는 비-상수 operator[]를 제공한다 (const 버전의 operator[]는 생략한다. 언제나 rvalue이므로 구별하지 않아도 되기 때문이다).

    class Lines
    {
        std::vector<std::string> d_line;

        public:
            Lines(std::istream &in);
            std::string &operator[](size_t idx);
    };

lvalue와 rvalue 사이를 구별하기 위하여 그 사이에 이용할 수 있는 특징을 발견해야 한다. 그런 구분 특징은 (언제나 lvalue로 사용되는) operator= 연산자 그리고 (언제나 rvalue로 사용되는) 변환 연산자이다. operator[]에게 string &을 돌려주도록 하는 대신에 Proxy 객체를 돌려주도록 만들 수 있다. 이 객체는 lvalue로 사용되는지 rvalue로 사용되는지 구별할 수 있다.

그래서 Proxy 클래스는 (lvalue로 행위하는) operator=(string const &other)와 그리고 (rvalue로 행위하는) operator std::string const &() const가 필요하다. 연산자가 더 필요할까? std::string 클래스는 operator+=로 제공한다. 그래서 이 연산자도 구현해야 할 듯 싶다. 평범한 문자들도 string 객체에 할당할 수 있다 (심지어 숫치 값을 할당해도 된다). string 객체는 평범한 문자로부터 생성할 수 없기 때문에 오른쪽 인자가 문자라면 operator=(string const &other)와 함께승격을 사용할 수 없다. 그러므로 operator=(char value)를 구현하는 것도 고려할 수 있다. 이 추가 연산자들은 현재 구현에서 빠졌지만 `실 세계의' 프록시 클래스라면 구현하는 것을 고려해야 한다.

또다른 미묘한 점은 ostream의 삽입 연산자나 istream의 추출 연산자를 사용할 때 Proxyoperator std::string const &() const가 사용되지 않는다는 것이다. 이 연산자들은 우리의 Proxy 클래스 유형을 인지하지 못하는 템플릿으로 구현되어 있기 때문이다. 그래서 스트림 삽입과 추출이 요구되면 (아마도 그럴 것이다) Proxy에 따로 중복정의 삽입 연산자와 추출 연산자를 구현해야 한다. 다음은 중복정의 삽입 연산자 구현이다. 대신할 객체를 Proxy가 삽입한다.

inline std::ostream &operator<<(std::ostream &out, Lines::Proxy const &proxy)
{
    return out << static_cast<std::string const &>(proxy);
}

(Lines를 제외하고) Proxy객체를 복사하거나 생성하는 데 코드가 전혀 필요없다. 그러므로 Proxy의 생성자는 비밀로 만들어야 하고 ProxyLines를 친구로 선언할 수 있다. 사실, ProxyLines에 밀접하게 관련되어 있으며 내포 클래스로 정의할 수 있다. 개선된 Lines 클래스에서 operator[] 연산자는 더 이상 string을 돌려주지 않는다. 대신에 Proxy를 돌려준다. 다음은 개선된 Lines 클래스로서 Proxy 클래스를 안에 내포한다.

    class Lines
    {
        std::vector<std::string> d_line;

        public:
            class Proxy;
            Proxy operator[](size_t idx);
            class Proxy
            {
                friend Proxy Lines::operator[](size_t idx);
                std::string &d_str;
                Proxy(std::string &str);
                public:
                    std::string &operator=(std::string const &rhs);
                    operator std::string const &() const;
            };
            Lines(std::istream &in);
    };

Proxy의 멤버들은 아주 가볍다. 그리고 인라인으로 구현할 수 있다.

    inline Lines::Proxy::Proxy(std::string &str)
    :
        d_str(str)
    {}
    inline std::string &Lines::Proxy::operator=(std::string const &rhs)
    {
        return d_str = rhs;
    }
    inline Lines::Proxy::operator std::string const &() const
    {
        return d_str;
    }

Lines::operator[] 멤버도 인라인으로 구현할 수 있다. 그저 인덱스 idx와 연관된 string으로 초기화하여 Proxy 객체를 돌려주기만 하면 된다.

이제 Proxy 클래스를 개발했으므로 프로그램에 사용할 수 있다. 다음은 Proxy 객체를 lvalue 또는 rvalue로 사용하는 예제이다. 겉보기에 Lines 객체는 원래의 구현을 사용하는 Lines 객체와 별다르게 행위하지 않는다. 그러나 Proxy의 멤버에 식별하는 cout 서술문을 추가해 보면 operator[] 연산자가 lvalue로 사용될 때와 rvalue로 사용될 때 서로 다르게 행위하는 것을 볼 수 있다.

    int main()
    {
        ifstream in("lines.cc");
        Lines lines(in);

        string s = lines[0];        // rvalue 사용
        lines[0] = s;               // lvalue 사용
        cout << lines[0] << '\n';   // rvalue 사용
        lines[0] = "hello world";   // lvalue 사용
        cout << lines[0] << '\n';   // rvalue 사용
    }

24.7: `reverse_iterator' 구현하기

22.14.1항에서 반복자와 역방향 반복자의 생성 방법을 논의했다. 그 항에서는 문자열을 가리키는 포인터 벡터로부터 파생된 클래스 안에 반복자를 내부 클래스로 생성했다.

이렇게 내포된 반복자 클래스의 객체는 벡터 안에 저장된 포인터의 역참조를 처리한다. 이 덕분에 포인터가 아니라 벡터의 원소가 가리키는 문자열을 정렬할 수 있다.

이 방법의 단점은 반복자를 구현한 클래스가 파생 클래스와 긴밀하게 묶인다는 것이다. 반복자 클래스가 내포 클래스로 구현되었기 때문이다. 포인터를 저장한 컨테이너 클래스로부터 파생된 클래스에 포인터-역참조를 처리하는 반복자를 제공하고 싶다면 어떻게 할 것인가?

이 절은 이전의 (내포 클래스) 접근법을 다양하게 논의한다. 여기에서 반복자 클래스는 클래스 템플릿으로 정의된다. 컨테이너의 요소들이 가리키는 데이터 유형은 물론이고 컨테이너 반복자 유형 자체가 가리키는 데이터 유형을 매개변수화한다. 다시 한 번 RandomIterator의 개발에 집중하자. 가장 복잡한 반복자 유형이기 때문이다.

클래스는 이름이 RandomPtrIterator인데, 이름 그대로 포인터 값에 작동하는 무작위 반복자이다. 클래스 템플릿에 템플릿 유형 매개변수가 세 가지 정의된다.

RandomPtrIterator는 비공개 BaseIterator 데이터 멤버가 하나 있다. 다음은 클래스 인터페이스와 생성자 구현이다.
    #include <iterator>

    template <typename Class, typename BaseIterator, typename Type>
    class RandomPtrIterator:
          public std::iterator<std::random_access_iterator_tag, Type>
    {
        friend RandomPtrIterator<Class, BaseIterator, Type> Class::begin();
        friend RandomPtrIterator<Class, BaseIterator, Type> Class::end();

        BaseIterator d_current;

        RandomPtrIterator(BaseIterator const &current);

        public:
            bool operator!=(RandomPtrIterator const &other) const;
            int operator-(RandomPtrIterator const &rhs) const;
            RandomPtrIterator operator+(int step) const;
            Type &operator*() const;
            bool operator<(RandomPtrIterator const &other) const;
            RandomPtrIterator &operator--();
            RandomPtrIterator operator--(int);
            RandomPtrIterator &operator++();
            RandomPtrIterator operator++(int);
            bool operator==(RandomPtrIterator const &other) const;
            RandomPtrIterator operator-(int step) const;
            RandomPtrIterator &operator-=(int step);
            RandomPtrIterator &operator+=(int step);
            Type *operator->() const;
    };

    template <typename Class, typename BaseIterator, typename Type>
    RandomPtrIterator<Class, BaseIterator, Type>::RandomPtrIterator(
                                    BaseIterator const &current)
    :
        d_current(current)
    {}

friend 선언을 보면 Class, BaseIterator, Type 유형에 대하여 RandomPtrIterator 객체를 돌려주는 Class 클래스의 begin 멤버와 end 멤버가 RandomPtrIterator의 비공개 생성자에 접근하는 것을 허용한다. 정확하게 우리가 원하는 것이다. Classbegin 멤버와 end 멤버는 묶인 친구로 선언된다.

RandomPtrIterator의 나머지 모든 멤버는 공개이다. RandomPtrIterator22.14.1항에서 개발한 iterator 내포 클래스를 일반화한 것일 뿐이기 때문에 필요한 멤버 함수를 재구현하는 것은 어렵지 않다. 그냥 iteratorRandomPtrIterator로 바꾸고 std::stringType으로 바꾸기만 하면 된다. 예를 들어 iterator 클래스에 다음과 같이 정의된 operator<

inline bool StringPtr::iterator::operator<(iterator const &other) const
{
    return d_current < other.d_current;
}

이제 다음과 같이 구현된다.

    template <typename Class, typename BaseIterator, typename Type>
    bool RandomPtrIterator<Class, BaseIterator, Type>::operator<(
                                    RandomPtrIterator const &other) const
    {
        return **d_current < **other.d_current;
    }

다음에 예를 몇 가지 더 보여준다. iterator 클래스에 다음과 같이 정의된 operator*는 이제

    inline std::string &StringPtr::iterator::operator*() const
    {
        return **d_current;
    }

다음과 같이 구현된다.

    template <typename Class, typename BaseIterator, typename Type>
    Type &RandomPtrIterator<Class, BaseIterator, Type>::operator*() const
    {
        return **d_current;
    }

전위 증가 연산자와 후위 증가 연산자는 이제 다음과 같이 구현된다.

    template <typename Class, typename BaseIterator, typename Type>
    RandomPtrIterator<Class, BaseIterator, Type>
    &RandomPtrIterator<Class, BaseIterator, Type>::operator++()
    {
        ++d_current;
        return *this;
    }
    template <typename Class, typename BaseIterator, typename Type>
    RandomPtrIterator<Class, BaseIterator, Type>
    RandomPtrIterator<Class, BaseIterator, Type>::operator++(int)
    {
        return RandomPtrIterator(d_current++);
    }

나머지 멤버는 그에 맞게 구현할 수 있다. 실제 구현은 독자 여러분에게 연습 문제로 남긴다 (아니면, 물론 cplusplus.yo.zip 압축파일에서 얻을 수 있다).

22.14.1항에서 개발한 StringPtr 클래스를 재구현하는 것도 역시 어렵지 않다. RandomPtrIterator 클래스 템플릿을 정의한 헤더를 포함하는 것을 빼면 한 곳만 변경하면 된다. iterator에 대하여 typedef는 이제 RandomPtrIterator와 연관지어야 한다. 다음은 완전한 클래스 인터페이스와 클래스의 인라인 멤버 정의들이다.

    #ifndef INCLUDED_STRINGPTR_H_
    #define INCLUDED_STRINGPTR_H_

    #include <vector>
    #include <string>
    #include "iterator.h"

    class StringPtr: public std::vector<std::string *>
    {
        public:
            typedef RandomPtrIterator
                    <
                        StringPtr,
                        std::vector<std::string *>::iterator,
                        std::string
                    >
                        iterator;

            typedef std::reverse_iterator<iterator> reverse_iterator;

            iterator begin();
            iterator end();
            reverse_iterator rbegin();
            reverse_iterator rend();
    };

    inline StringPtr::iterator StringPtr::begin()
    {
        return iterator(this->std::vector<std::string *>::begin() );
    }
    inline StringPtr::iterator StringPtr::end()
    {
        return iterator(this->std::vector<std::string *>::end());
    }
    inline StringPtr::reverse_iterator StringPtr::rbegin()
    {
        return reverse_iterator(end());
    }
    inline StringPtr::reverse_iterator StringPtr::rend()
    {
        return reverse_iterator(begin());
    }
    #endif

StringPtr의 변경된 헤더 파일을 22.14.2항에 주어진 프로그램에 포함하기만 하면 그 결과 프로그램은 이전 버전과 동일하게 행위한다. 이 경우 StringPtr::beginStringPtr::end는 템플릿 정의로부터 생성된 반복자 객체를 돌려준다.

24.8: `bisonc++'와 `flexc++' 사용하기

아래에 논의할 예제는 C++ 소스를 생성하는 파서 생성기와 스캐너 생성기를 사용함에 있어서 특이한 점들을 파헤친다. 한 프로그램에 입력이 일정 수준의 복잡성을 초과하면 스캐너 생성기와 파서 생성기를 사용하여 실제 입력을 인지하는 코드를 생성하는 것이 좋다.

이 절과 다음 절의 예제는 여러분이 스캐너 생성기 flex파서 생성기 bison를 사용하는 법을 알고 있다고 가정한다. 두 생성기 bisonflex 모두 다른 곳에 문서화가 잘 되어 있다. bisonflex의 원조인 yacclex는 여러 책에 잘 기술되어 있다. 예를 들어 오라일리(O'Reilly)사의 책 `lex & yacc'를 참고하라.

스캐너 생성기와 파서 생성기는 무료 소프트웨어이다. bisonflex 둘 다 배포본에 표준으로 포함된다. 아니면 ftp://prep.ai.mit.edu/pub/non-gnu으로부터 얻을 수 있다. %option c++ 옵션을 지정하면 FlexC++ 클래스를 생성한다.

파서 생성기라면 bison 프로그램을 사용할 수 있다. 90년대 초, 알레인 꼬에모(Alain Coetmeur)는 (coetmeur@icdc.fr) 파서 클래스를 생성하는 (bison++) C++ 변형을 만들었다. bison++ 프로그램이 만든 코드는 C++ 프로그램에 사용할 수 있지만 C++ 문맥보다 C 문맥에 더 적합한 특징들도 많이 보여준다. 2005년 1월 필자는 알레인(Alain)의 bison++ 프로그램 일부를 재작성했다. 그 결과로 최초의 bisonc++ 버전이 탄생했다. 그 후 2005년 5월 완전히 재작성된 bisonc++ 파서 생성기가 완성되었다 (버전 번호 0.98). bisonc++의 현재 버전은 https://fbb-git.github.io/bisoncpp/으로부터 내려받을 수 있다. (bisonc++의 문서를 비롯하여) 소스와 그리고 이진 (i386) 데비안 패키지를 얻을 수 있다.

Bisonc++bison++보다 파서 클래스를 더 깔끔하게 만들어 낸다. 특히, 파서 클래스를 바탕 클래스로부터 상속받는다. 안에 파서의 토큰 정의와 유형 정의를 비롯하여 프로그래머가 (재)정의하면 안되는 멤버들이 모두 들어 있다. 이 접근법의 결과로 아주 작은 파서 클래스가 생성된다. 프로그래머가 실제로 정의한 멤버들만 선언한다 (bisonc++ 자체에서 생성하는 어떤 멤버는 파서의 parse() 멤버를 구현한다). 기본으로 구현되지 않는 멤버 하나는 다음 어휘 토큰을 생산하는 lex이다. %scanner (24.8.2.1목) 지시어를 사용할 때 bisonc++는 이 멤버를 표준으로 구현한다. 그렇지 않으면 프로그래머가 구현해야 한다.

2012년 초에 flexc++ https://fbb-git.github.io/flexcpp/ 프로그램은 최초 배포 단계에 이르렀다. bisonc++처럼 데비안 리눅스 배포본에 포함되어 있다.

장-폴 반 우스텐(Jean-Paul van Oosten (j.p.van.oosten@rug.nl))과 리차드 베렌센(Richard Berendsen (richardberendsen@xs4all.nl))은 flexc++ 프로젝트를 2008년에 시작했다. 그리고 최종 프로그램이 장-폴(Jean-Paul)과 필자에 의하여 2010년과 2012년 사이에 완성되었다.

이 절은 파서 생성기로서 bisonc++에 그리고 어휘 스캐너 생성기로서 flexc++ 초점을 둔다. 이 책의 이전 배포본에서는 flex를 스캐너 생성기로 사용했었다.

flex++bisonc++를 사용하여 class-기반의 스캐너와 파서를 생성한다. 이 접근법의 장점은 class 인터페이스를 사용하지 않을 때보다 스캐너와 파서에 대한 인터페이스가 더 깔끔해진다는 것이다. 게다가 클래스 덕분에 전역 변수를 대부분 제거할 수 있으므로 한 프로그램에 여러 파서를 쉽게 사용할 수 있게 된다.

아래에 예제 프로그램을 두 가지 개발한다. 첫 예제는 flexc++만 사용한다. 생성된 스캐너는 여러 부분으로부터 파일을 생산하는 것을 관제한다. 이 예제는 정보를 휘젓고 돌아다니는 동안 어휘 스캐너와 파일 사이의 전환에 초점이 있다. 두 번째 예제는 flexc++bisonc++를 모두 사용하여 스캐너와 파서를 생성한다. 표준 산술 표현식을 후위 표현식으로 변환한다. 후위 표현식은 컴파일러가 생산하는 코드와 HP-계산기에 주로 사용된다. 두 번째 예제는 주로 bisonc++에 초점이 있고 생성된 파서 안에서 스캐너 객체를 조합하는 것에 중점이 있다.

24.8.1: `flexc++'를 사용하여 스캐너 만들기

이 항에서 개발된 어휘 스캐너는 여러 부파일로부터 파일을 하나 만들어 내는 것을 관제한다. 설정은 다음과 같다. 입력-언어는 파일 경로를 지정하는 텍스트 문자열을 정의한다. #include 지시어 다음에 이 파일을 포함해야 한다.

우리의 예제는 쓸데없는 복잡성을 피하기 위해 #include 서술문의 형식을 #include <filepath>으로 제한한다. 옆꺽쇠 사이에 지정된 파일은 filepath에 지시된 위치에서 사용할 수 있어야 한다. 그 파일을 사용할 수 없으면 프로그램은 에러 메시지를 보여주며 종료한다.

프로그램은 한 두 개의 파일이름을 인자로 하여 시작한다. 프로그램이 단 하나의 파일 인자로 시작하면 출력은 표준 출력 스트림 cout에 씌여진다. 그렇지 않으면 출력은 프로그램의 두 번째 인자에 주어진 이름의 스트림에 씌여진다.

프로그램은 최대 내포 깊이를 정의한다. 이 최대 깊이를 초과하면 프로그램은 에러 메시지를 보여주고 종료된다. 그 경우, 파일이 포함된 위치를 알려주는 파일이름 스택이 출력된다.

프로그램의 추가 특징은 (표준 C++) 주석줄을 무시한다는 것이다. 명령줄 안의 include-지시어들도 역시 무시된다.

프로그램은 다섯 단계로 생성된다.

24.8.1.1: `Scanner' 파생 클래스

정규 표현식 규칙에 부합시키는 lex 함수는 Scanner 클래스의 멤버이다. Scanner 클래스는 ScannerBase 클래스로부터 파생되었으므로 어휘 스캐너의 정규 표현식 부합 알고리즘을 실행하는 ScannerBase 클래스의 모든 보호 멤버에 접근할 수 있다.

정규 표현식 자체를 보면 주석과 #include 지시어 그리고 나머지 모든 문자들을 인지하는 규칙이 필요함을 알 수 있다. 이 모든 것은 상당히 표준적인 관례이다. #include 지시어를 감지하면 그 지시어는 스캐너가 해석한다. 이 또한 보통의 관례이다. 우리의 어휘 스캐너는 다음과 같은 일을 한다.

24.8.1.2: 어휘 스캐너 규격 파일

어휘 스캐너 규격 파일은 C 문맥에서 flex에 사용된 규격 파일과 비슷하게 조직된다. 그렇지만 C++ 문맥에서 flexc++는 단순히 스캐너 함수가 아니라 Scanner 클래스를 생성한다.

Flexc++의 규격 파일은 두 부분으로 구성된다.

24.8.1.3: `Scanner' 구현하기

Scanner 클래스는 flexc++에 의하여 한 번 생성된다. 이 클래스는 ScannerBase 바탕 클래스가 정의한 여러 멤버에 접근한다. 이 멤버 중 어떤 멤버는 공개 접근 권한이 있고 Scanner 클래스의 외부 코드에 사용할 수 있다. 이 멤버들은 flexc++(1) 매뉴얼 페이지에 널리 문서화되어 있다. 더 자세한 정보는 이를 참고하라.

우리의 스캐너는 다음 일을 수행한다.

입력에서 #include 서술문으로 스캐너는 스캔 처리를 계속해야 하는 파일이름을 추출할 수 있다. 이 파일이름은 d_nextSource 지역 변수에 저장된다. 그리고 stackSource 멤버는 다음 소스로의 전환을 처리한다. 다른 것은 요구되지 않는다. 입력 파일을 넣고 빼는 것은 flexc++가 제공하는 스캐너의 pushStream 멤버와 popStream멤버가 처리한다. 그러므로 Scanner의 인터페이스는 switchSource 함수 하나만 더 선언하면 된다.

스트림 전환은 다음과 같이 처리된다. 스캐너가 파일이름을 #include 지시어로부터 추출하면 switchSource에 의하여 또다른 파일로 전환된다. 이 멤버는 flexc++가 정의한 pushStream 멤버를 호출하여 현재 입력 스트림을 스택에 넣고 d_nextSource 변수에 저장된 이름의 스트림으로 전환한다. 이것은 또 include 미니-스캐너를 끝낸다. 그래서 스캐너를 기본 스캐닝 모드로 돌려주기 위해 begin(StartCondition__::INITIAL) 함수가 호출된다. 다음은 그 소스이다.

#include "scanner.ih"

void Scanner::switchSource()
{
    pushStream(d_nextSource);
    begin(StartCondition__::INITIAL);
}

flexc++가 정의한 pushStream 멤버는 필요한 모든 점검을 처리하고 파일을 열 수 없거나 스택에 파일이 너무 많으면 예외를 던진다.

flexc++Scanner::lex에 어휘를 스캔하는 멤버를 정의한다. 이 멤버를 호출하면 스캐너가 돌려주는 토큰을 처리할 수 있다.

24.8.1.4: `Scanner' 객체 사용하기

Scanner를 사용하는 프로그램은 아주 단순하다. 스캔 처리를 시작할 곳을 알려주는 파일이름을 기대한다.

프로그램은 먼저 인자의 갯수를 점검한다. 인자가 하나라도 주어지면 그 인자는 두 번째 인자 "-"와 함께 Scanner의 생성자에 건네진다. 이 두 번째 인자는 출력이 표준 출력 스트림으로 가야한다고 알려준다.

프로그램이 인자를 두 개 이상 받으면 디버그 출력도 표준 출력 스트림에 씌여진다. 디버그 출력에 어휘 스캐너의 조치가 문서화된다.

다음으로 Scannerlex 멤버가 호출된다. 무엇이든 실패하면 std::exception 예외가 던져진다. 이 예외는 main의 try-블록에서 catch 절이 잡는다. 다음은 프로그램의 소스이다.

#include "lexer.ih"

int main(int argc, char **argv)
try
{
    if (argc == 1)
    {
        cerr << "Filename argument required\n";
        return 1;
    }

    Scanner scanner(argv[1], "-");

    scanner.setDebug(argc > 2);

    return scanner.lex();
}
catch (exception const &exc)
{
    cerr << exc.what() << '\n';
    return 1;
}

24.8.1.5: 프로그램 빌드하기

최종 프로그램은 두 단계로 생성된다. 이 두 단계는 유닉스 시스템을 위한 것이다. 유닉스 시스템에는 flexc++Gnu C++ 컴파일러 g++이 설치되어 있다. Flexc++https://fbb-git.github.io/flexcpp/으로부터 내려받을 수 있다. 그리고 bobcat 라이브러리도 필요하다. http://bobcat.sf.net/에서 내려받을 수 있다.

24.8.2: `bisonc++'와 `flexc++' 사용하기

입력 언어가 일정 정도의 복잡성을 넘어서면 파서를 사용해 그 언어의 복잡성을 제어하는 경우가 많다. 이 경우에 파서 생성기를 사용하여 입력의 문법적 정확성을 검증하는 코드를 생성할 수 있다. 어휘 스캐너는 (파서 안으로 조합해 넣으면 더 좋은) 토큰이라고 부르는 입력 의미조각을 제공한다. 그러면 파서는 어휘 스캐너가 생성한 일련의 토큰을 처리한다.

파서와 스캐너를 모두 사용하는 프로그램을 개발할 때 시발점은 문법이다. 문법에 정의된 토큰 집합은 어휘 스캐너가 돌려줄 수 있다 (이하 스캐너라고 지칭).

마지막으로, 빈 공백을 채우기 위해 보조 코드가 제공된다. 파서와 스캐너가 수행하는 조치들은 정상적으로 지정되지 않는다. 문자 그대로 문법 규칙이나 어휘 정규 표현식으로 지정되지 않는다. 파서의 규칙들이 호출할 수 있도록 멤버 함수들 안에 구현해 넣거나 스캐너의 정규 표현식에 연관지어 주어야 한다.

이전 절에서 flexc++가 생성하는 C++ 클래스의 예제를 보았다. 현재 절에서는 파서에 집중하겠다. 파서는 bisonc++ 프로그램이 처리해 주는 문법 지정 파일로부터 생성할 수 있다. bisonc++가 요구하는 문법 지정 파일은 bison(bison++)이 처리하는 파일과 비슷하다 (bison++ 파서는 bisonc++의 전신으로서 19세기 초에 알레인 꼬에모(Alain Coetmeur)가 작성했다).

이 절은 중위 표현식후위 표현식으로 변환하는 프로그램을 개발한다. 중위 표현식은 이항 연산자가 피연산자 사이에 있고 후위 표현식은 이항 연산자가 피연산자 뒤에 위치한다. 또한 단항 연산자 -는 전위 표기법에서 후위 표기법으로 변환된다. 단항 연산자 +는 무시한다. 더 이상 처리할 필요가 없기 때문이다. 본질적으로 우리의 작은 계산기는 마이크로 컴퓨터이다. 숫치 표현식을 어셈블리-류의 명령어로 변환한다.

우리의 계산기는 기본적인 연산자 집합을 인지한다. 곱셈, 덧셈, 괄호, 그리고 단항 마이너스를 인지한다. 실수를 정수와 구별하겠다. bison-류의 문법 지정에 있어서 미묘한 점들을 보여주기 위해서이다. 이상이다. 이 절의 목적은 결국 파서와 어휘 스캐너를 모두 사용하는 C++ 프로그램의 생성 방법을 보여주는 것이다. 완벽하게 기능을 갖춘 계산기를 만드는 데 목적이 있지 않다.

다음 목에서는 bisonc++를 위하여 문법을 지정하는 방법을 개발할 것이다. 다음, 스캐너를 위한 정규 표현식을 지정한다. 그 다음에 최종 프로그램을 만든다.

24.8.2.1: `bisonc++' 규격 파일

bisonc++가 요구하는 문법 지정 파일은 bison이 요구하는 규격 파일과 비슷하다. 차이점은 결과 파서 클래스의 성격에 있다. 우리의 계산기는 정수와 실수를 구별한다. 그리고 기본적인 산술 연산자 집합을 지원한다.

Bisonc++는 다음과 같이 사용해야 한다.

bisonc++ 규격 파일은 두 부분으로 구성된다.

bison에 익숙한 독자분이라면 헤더 부분이 더 이상 없다는 사실을 눈치채셨을 것이다. 헤더 부분은 bison이 사용하여 필요한 선언을 제공한다. 이를 이용하여 컴파일러는 bison이 생성한 C 함수를 컴파일할 수 있다. C++에서 선언은 클래스 정의의 일부이거나 아니면 이미 사용 중이다. 그러므로 파서 생성기는 C++ 클래스를 생성하고 멤버 함수 중 어떤 멤버는 헤더 부분을 더 이상 요구하지 않는다.
선언 부분
선언 부분에 여러 선언 집합이 들어 있다. 문법에 사용된 모든 토큰들과 우선순위 그리고 수학 연산자 등등과 관계가 있다. 게다가 중요한 여러 규격도 새로 여기에 넣을 수 있다. 현재 예제에 관련 있는 것들과 bisonc++에서만 사용할 수 있는 것들을 여기에서 논의한다. 완전한 설명은 bisonc++의 매뉴얼 페이지를 참조하시기를 바란다. %union 선언의 예는 다음과 같다.
    %union
    {
        int     i;
        double  d;
    };
C++11 이전의 코드라면 공용체는 자신의 필드에 객체를 가질 수 없다. 공용체를 생성할 때 생성자를 호출할 수 없기 때문이다. 이것은 string이 공용체의 멤버가 될 수 없다는 뜻이다. 그렇지만 string *은 공용체의 멤버로 가능하다. 또 클래스 유형의 객체를 필드로 가질 수 있는 무제한 공용체를 사용하는 것도 가능하다 (12.6절).

사실 스캐너는 그런 공용체에 관하여 알 필요가 없다. 그저 스캔된 텍스트를 matched 멤버 함수를 통하여 파서에 건넬 수만 있으면 된다. 예를 들어 다음과 같은 서술문은

    $$.i = A2x(d_scanner.matched());
부합된 텍스트를 적절한 유형의 값으로 변환한다.

토큰과 비-터미날은 공용체 필드에 연관지을 수 있다. 이것을 적극 권장한다. 그러면 컴파일러가 유형이 올바른지 점검하기 때문에 유형 불일치를 방지해 준다. 뿐만 아니라 bison에 종속적인 변수 $$, $1, $2, 등등을 사용할 수 있다. ($$.i처럼) 필드를 완전하게 지정하지 않아도 된다. 비-터미날 또는 토큰은 공용체 필드에 연관지을 수 있다. <fieldname>을 지정하면 된다. 예를 들어,

    %token <i> INT          // 토큰 연관 (비추천, 아래 참고)
           <d> DOUBLE
    %type  <i> intExpr      // 비-터미날 연관
여기에서 개발된 예제는 토큰과 비-터미날 모두 공용체 필드에 연관지을 수 있다. 그렇지만 앞서 지적했듯이 스캐너는 이 모든 것에 관하여 알 필요가 없다. 스캐너에게 한 가지 일만 시키는 것이 더 깔끔할 것이다. 텍스트를 스캔하는 일만 시키는 게 좋다. 그 대신 입력이 무엇인지 잘 아는 파서"123" 같은 문자열을 정수 값으로 변환할 수 있다. 결론적으로 공용체 필드와 토큰을 연관짓는 것은 권장하지 않는다. 아래에서 문법 규칙을 기술하는 동안 이 점을 더 자세히 보여준다.

%union을 논의하면서 %token%type 규격을 지적해야겠다. 스캐너가 돌려줄 수 있는 토큰을 (즉, 터미널 심볼을) 지정하고 비-터미날의 반환 유형을 지정한다. %token 말고도 토큰 선언자로서 %left%right 그리고 %nonassoc를 사용하여 연산자의 연관성을 지정할 수 있다. 이 지시어에 언급된 토큰들은 연산자를 나타내는 토큰으로 번역되어, 지시한 방향에 연관된다. 연산자의 우선 순위는 순서대로 정의된다. 첫 번째 지정된 연산자가 우선 순위가 제일 낮다. 특정한 문맥에서 우선 순위를 바꾸려면 %prec을 사용할 수 있다. 이 모든 것은 표준 bisonc++ 관례이기 때문에 여기에서 더 이상 언급하지 않는다. 더 자세한 정보는 bisonc++의 배포본에 포함된 문서를 참조하시기를 바란다.

다음은 계산기의 선언 부분의 규격이다.

%filenames parser
%scanner ../scanner/scanner.h

%union {
    int i;
    double d;
};

%token  INT DOUBLE

%type   <i> intExpr
%type   <d> doubleExpr

%left   '+'
%left   '*'
%right  UnaryMinus

선언 부분에 %type 지정자가 사용된다. intExpr 규칙의 값을 (다음 절 참고) 의미구조-값 공용체의 i-필드에 연관짓고 doubleExpr의 값을 d-필드에 연관짓는다. 이 접근법은 솔직히 말해 좀 복잡하다. 지원되는 공용체 유형마다 표현식 규칙을 포함해야 하기 때문이다. 물론 다른 방법이 있다. 그리고 다형적 의미구조 값의 사용이 관련된다. 이에 관해서는 Bisonc++ 사용자 가이드에 더 자세하게 다룬다.

문법 규칙
문법의 규칙과 행위는 예와 같이 지정된다. 우리의 계산기를 위한 문법은 아래에 기술되어 있다. 몇 가지 규칙이 있지만 그 규칙들은 bisonc++이 제공하는 다양한 특징들을 보여준다. 특히 한 줄의 코드 말고는 어떤 행위 블록도 요구하지 않는다는 것을 눈여겨보라. 이 덕분에 문법이 단순하다. 그러므로 가독성과 이해도가 개선된다. 파서의 적당한 종료를 정의한 규칙조차도 (line 규칙에 있는 빈줄) done이라고 부르는 멤버 함수 하나만 사용한다. 그 함수의 구현은 간단한다. 그러나 Parser::ACCEPT를 호출한다는 사실을 눈여겨볼 가치가 있다. ACCEPT를 생산 규칙의 행위 블록으로부터 간접적으로 호출한다는 것을 보여준다. 다음은 문법의 생산 규칙이다.
    lines:
        lines
        line
    |
        line
    ;

    line:
        intExpr
        '\n'
        {
            display($1);
        }
    |
        doubleExpr
        '\n'
        {
            display($1);
        }
    |
        '\n'
        {
            done();
        }
    |
        error
        '\n'
        {
            reset();
        }
    ;

    intExpr:
        intExpr '*' intExpr
        {
            $$ = exec('*', $1, $3);
        }
    |
        intExpr '+' intExpr
        {
            $$ = exec('+', $1, $3);
        }
    |
        '(' intExpr ')'
        {

            $$ = $2;
        }
    |
        '-' intExpr         %prec UnaryMinus
        {
            $$ = neg($2);
        }
    |
        INT
        {
            $$ = convert<int>();
        }
    ;

    doubleExpr:
        doubleExpr '*' doubleExpr
        {
            $$ = exec('*', $1, $3);
        }
    |
        doubleExpr '*' intExpr
        {
            $$ = exec('*', $1, d($3));
        }
    |
        intExpr '*' doubleExpr
        {
            $$ = exec('*', d($1), $3);
        }
    |
        doubleExpr '+' doubleExpr
        {
            $$ = exec('+', $1, $3);
        }
    |
        doubleExpr '+' intExpr
        {
            $$ = exec('+', $1, d($3));
        }
    |
        intExpr '+' doubleExpr
        {
            $$ = exec('+', d($1), $3);
        }
    |
        '(' doubleExpr ')'
        {
            $$ = $2;
        }
    |
        '-' doubleExpr         %prec UnaryMinus
        {

            $$ = neg($2);
        }
    |
        DOUBLE
        {
            $$ = convert<double>();
        }
    ;

이 문법은 간단한 계산기를 구현한다. 정수 값과 실수 값을 부인할 수 있고 더하고 곱할 수 있다. 그리고 표준 우선 순위 규칙은 괄호로 바꿀 수 있다. 문법은 유형이 있는 비-터미날 심볼의 사용을 보여준다. doubleExpr는 실수 (배정도) 값에 연결되고 intExpr는 정수 값에 연결된다. 우선순위와 유형 연결은 파서의 정의 부분에 정의된다.

파서의 헤더 파일
문법으로부터 호출된 여러 클래스 멤버는 멤버 템플릿으로 정의된다. Bisonc++는 여러 파일을 생성한다. 그 중에 파서 클래스를 정의하는 파일이 있다. 생성 규칙의 조치 블록으로부터 호출된 함수는 파서의 멤버 함수이다. 이 멤버 함수들을 선언하고 정의해야 한다. 일단 bisonc++가 파서의 클래스를 정의한 헤더 파일을 생산하고 나면 그 헤더 파일은 자동으로 다시 씌여지지 않는다. 덕분에 프로그래머는 필요할 때마다 새 멤버를 파서 클래스에 추가할 수 있다. 다음은 우리의 작은 계산기에 사용된 `parser.h' 이다.
#ifndef Parser_h_included
#define Parser_h_included

#include <iostream>
#include <sstream>
#include <bobcat/a2x>

#include "parserbase.h"
#include "../scanner/scanner.h"

#undef Parser
class Parser: public ParserBase
{
    std::ostringstream d_rpn;
    // $insert scannerobject
    Scanner d_scanner;

    public:
        int parse();

    private:
        template <typename Type>
        Type exec(char c, Type left, Type right);

        template <typename Type>
        Type neg(Type op);

        template <typename Type>
        Type convert();

        void display(int x);
        void display(double x);
        void done() const;
        void reset();
        void error(char const *msg);
        int lex();
        void print();

        static double d(int i);

    // parse()에 함수들을 지원함:

        void executeAction(int d_ruleNr);
        void errorRecovery();
        int lookup(bool recovery);
        void nextToken();
        void print__();
};

inline double Parser::d(int i)
{
    return i;
}

template <typename Type>
Type Parser::exec(char c, Type left, Type right)
{
    d_rpn << " " << c << " ";
    return c == '*' ? left * right : left + right;
}

template <typename Type>
Type Parser::neg(Type op)
{
    d_rpn << " n ";
    return -op;
}

template <typename Type>
Type Parser::convert()
{
    Type ret = FBB::A2x(d_scanner.matched());
    d_rpn << " " << ret << " ";
    return ret;
}

inline void Parser::error(char const *msg)
{
    std::cerr << msg << '\n';
}

inline int Parser::lex()
{
    return d_scanner.lex();
}

inline void Parser::print()
{}

#endif

24.8.2.2: `flexc++' 규격 파일

계산기가 사용하는 flex-규격 파일은 단순하다. 빈 공간은 무시되고 단일 문자는 반환된다. 그리고 숫치 값은 Parser::INTParser::DOUBLE 토큰으로 반환된다.

flexc++ 지시어인 %interactive가 제공된다. 계산기는 사용자인 인간과 적극적으로 상호 작용하는 프로그램이기 때문이다.

다음은 완전한 flexc++ 규격 파일이다.

%interactive
%filenames scanner

%%

[ \t]                       // ignored

[0-9]+                      return Parser::INT;

"."[0-9]*                   |
[0-9]+("."[0-9]*)?          return Parser::DOUBLE;

.|\n                        return matched()[0];

24.8.2.3: 프로그램 빌드하기

계산기는 bisonc++flexc++를 사용하여 빌드한다. 다음은 계산기의 main 함수를 구현한다.
#include "parser/parser.h"

using namespace std;

int main()
{
    Parser parser;

    cout << "Enter (nested) expressions containing ints, doubles, *, + and "
            "unary -\n"
            "operators. Enter an empty line to stop.\n";

    return parser.parse();
}

다음 명령어는 파서의 parse.cc 파일과 parserbase.h 헤더를 만들어 낸다.

    bisonc++ grammar
parser.h 파일은 한 번만 생성된다. 개발자는 필요하면 멤버를 Parser 클래스에 추가할 수 있다.

flexc++ 프로그램은 구문 스캐너를 만든다.

    flexc++ lexer

유닉스 시스템에서 다음과 같은 명령어는

    g++ --std=c++14 -Wall -o calc *.cc -lbobcat -s
메인 프로그램의 소스와 스캐너 그리고 파서 생성기에 의하여 생성된 소스를 컴파일하고 링크할 수 있다. 예제는 A2x 클래스를 사용한다. 이 클래스는 bobcat 라이브러리에 포함되어 있다 (24.8.1.5목) (bobcat 라이브러리는 bisonc++이나 flexc++를 제공하는 시스템에서 사용할 수 있다). bisonc++는 다음에서 내려받을 수 있다.

https://fbb-git.github.io/bisoncpp/.