std::iostream 으로 std::tuple 입출력하기
Project/Win32API로 던그리드 모작

std::iostream 으로 std::tuple 입출력하기

Woon2World :: Programming & Art Life

 

istream& operator>>, ostream& operator<< 의 오버로딩

std::iostream 을 통해 다양한 primitive 타입을 입력받을 수 있다.

하지만 std::tuple 은 primitive 가 아니고, c++ 언어측에서 따로 std::istream 과 std::tuple 을 받는 operator>> 연산자를 오버로딩하지 않았으므로 std::istream 으로 std::tuple 을 입력받을 수는 없다.

같은 논리로 std::ostream 에 std::tuple 을 출력할 수도 없다.

 

따라서 std::istream 과 std::tuple 을 받는 operator>>사용자가 오버로딩하면 충분히 std::istream 으로 std::tuple 을 입력받을 수 있고, std::ostream 과 std::tuple 을 받는 operator<<사용자가 오버로딩하면 std::ostream 에 std::tuple 을 출력할 수 있다.

 

 


기존의 튜플 요소 입출력

#include <iostream>
#include <tuple>
#include <sstream>
 
using namespace std;
 
int main()
{
    stringstream ss;
    ss << "1 2 3 4 5";
    tuple< charshortlongintlong long > tup;
    ss >> get<0>( tup );
    ss >> get<1>( tup );
    ss >> get<2>( tup );
    ss >> get<3>( tup );
    ss >> get<4>( tup );
 
    cout << get<0>( tup ) << ", "
        << get<1>( tup ) << ", "
        << get<2>( tup ) << ", "
        << get<3>( tup ) << ", "
        << get<4>( tup ) << endl;
 
    // output : 1, 2, 3, 4, 5
}
cs

 

 

std::get 을 통해 일일이 입출력한다.

인덱스로 쓰이는 부분을 반복문으로 해결할 수 있을 것 같지만, 템플릿 인자컴파일 타임 상수여야 한다.

일반적으로 반복문에 int i 라고 써놓고 쓰는 인덱스는

  • 런타임에 생성된다.
  • 상수가 아니라 변수이다.

따라서 반복문을 통해 효율을 올리는 것이 불가능하고 위와 같이 입출력해야 한다.

 

 


가변 인자 템플릿의 가변 인자를 인덱싱하는 방법

반복문을 사용할 수 없는 이유를 거꾸로 뒤집으면 된다.

  • 컴파일 타임에 생성한다.
  • 변수가 아니라 상수를 이용한다.

그리고 이를 달성하는 방법은, 템플릿에 가변 길이 인자를 하나 더 추가인덱스 모음으로 삼는 것이다.

템플릿 문법은, 템플릿 문법으로 해결한다.

 

 


Pack Expansion

템플릿 인자는 기본적으로 타입이고, 템플릿 인자 목록은 타입 목록이다.

템플릿 가변 인자는 하나의 인자가 아니라 다수의 인자들을 묶는 묶음이다. 따라서 템플릿 가변 인자는 타입 목록 안 또 하나의 타입 목록이 된다.

 

템플릿 가변 인자는 타입이 아니라 타입 목록이므로, 템플릿 가변 인자 타입으로 변수를 선언할 수 없다.

묶음을 풀어서 그 안의 실제 타입을 꺼내와야 한다.

아쉽게도 배열처럼 operator[] 를 통해 특정 위치의 타입을 꺼내오기 같은 것은 불가능하다.

 

템플릿 가변 인자는 ... 를 통해 실제 타입들의 나열로 표현된다.

템플릿 가변 인자 Args 에 int, double, char 가 담겨져 있다면

Args... 는 실제 코드에서 int, double, char 로 확장된다.

마치 #define 으로 타입들을 하나하나 쉼표로 나열하는 매크로를 만든 것과 같다.

각 요소는 쉼표를 통해 구분되므로, 쉼표로 내용을 나열하여도 문제가 없는 문법적 맥락에서만 이용이 가능하다.

따라서 초기화 목록이나 함수의 인자 목록과 같은 한정된 지역에서만 이용이 가능하다.

 

 

#include <iostream>
 
using namespace std;
 
void increment() {}        // 재귀 종료용
 
template < typename Head, typename ... Args >
void increment( Head& head, Args&... args )
{
    ++head;
    increment( args... );
}
 
int main()
{
    int a = 1;
    double b = 2.3;
    char c = 'd';
 
    for ( int i = 0; i < 10++i )
        increment( a, b, c );
 
    cout << a << ", " << b << ", " << c << endl;
    // output : 11, 12.3, n
}
cs

 

 

보통 가변 인자 함수는 재귀함수로 구현된다.

템플릿 인자 목록은 타입 목록인데, 템플릿 가변 인자 또한 타입 목록이어서 위와 같은 활용이 가능하다.

함수로 들어온 인자 목록은 head 라는 인자와 args 라는 또 하나의 인자 목록으로 분리되며,

head 에 대해 처리를 한 뒤 남은 args 인자 목록으로 다시 자신을 재귀호출한다.

최종적으로 처리할 인자가 없으면 재귀를 종료하게 된다.

 

 


가변 길이 상수 목록 추가

일반적으로 가변 인자를 두 개를 받을 수는 없다.

가변 인자는 하나의 인자가 아니라 다수의 인자들을 묶는 묶음인데, 두 개 이상의 가변 인자를 사용하면 어느 부분까지가 어떤 가변 인자 속으로 들어가야 하고, 또 어느 부분까지가 어떤 가변 인자 속으로 들어가야 하는지 알 수 없기 때문이다.

 

하지만 템플릿의 경우 이것이 가능할 수 있다.

템플릿 인자는 기본적으로 타입이다. 템플릿 인자 목록은 타입 목록이다.

그런데 템플릿 인자로 받을 수 있는 것이 하나 더 있다.

바로 정수형 컴파일 타임 상수이다. 상수는 타입이 아니므로 타입 인자와 완전히 별개이다.

 

타입 가변 인자 하나를 받고, 또 타입 가변 인자 하나를 받는 일은 불가능하고

정수형 컴파일 타임 상수 가변 인자 하나를 받고, 또 정수형 컴파일 타임 상수 가변 인자 하나를 받는 일도 불가능하지만

위의 이유로 타입 가변 인자 하나를 받고, 정수형 컴파일 타임 상수 가변 인자 하나를 받는 것은 가능하다.

 

정수형 컴파일 타임 상수 가변 인자에 대해서도 타입 가변 인자처럼 pack expansion 을 통해 처리가 이루어진다.

정수형 컴파일 타임 상수 목록과 타입 목록, 둘은 "목록"으로서, ... 를 통해 그 안의 요소들을 확장한다.

타입 목록에서 타입을 하나하나 꺼내는 것처럼 정수형 컴파일 타임 상수 목록에서 정수형 컴파일 타임 상수를 하나하나 꺼낼 수 있다.

 

 

#include <iostream>
 
using namespace std;
 
template < size_t ... >
void increment() {}        // 재귀 종료용
 
template < size_t val, size_t ... restvals, typename Head, typename ... Args >
void increment( Head& head, Args&... args )
{
    head += val;
    increment< restvals... >( args... );
}
 
int main()
{
    int a = 0;
    double b = 0.0;
    long long c = 0;
 
    increment<123>( a, b, c );
 
    cout << a << ", " << b << ", " << c << endl;
    // output : 1, 2, 3
}
cs

 

 

정수형 컴파일 타임 상수 목록과 타입 목록은 동일하게 템플릿 가변 인자다.

따라서 템플릿 가변 인자에 적용되는 문법은 타입 목록에도, 정수형 컴파일 타임 상수 목록에도 적용할 수 있다.

 

 


index_sequence

std::index_sequence 라는 평소에는 보기 힘든 유별난 클래스가 있다.

이 클래스는 가변 인자 템플릿 함수의 요소를 인덱스로 순회할 때 사용한다.

 

std::index_sequence는 인덱싱에 쓰일 수 있는 컴파일 타임 정수 목록템플릿 인자로 받는다.

템플릿 인자 목록에 int, long, unsigned 등의 자료형으로 컴파일 타임 상수들을 나열하여 생성한다.

 

템플릿 함수는 호출에 쓰인 인자 목록을 바탕으로 템플릿 매개변수들을 추론한다.

함수에 std::index_sequence 객체를 생성하여 전달하면 인자 목록에 들어있는 이 객체를 보고 템플릿 매개변수들을 추론하기 시작하는데,

이 객체를 생성할 때 쓰인 정수들이 함수의 템플릿 인자로 하나씩 들어가게 된다.

함수가 컴파일 타임 정수 목록을 가변 인자로 가진다면, 그 목록으로 들어간다.

 

이 클래스의 힘은 std::make_index_sequence 클래스로부터 나온다.

std::index_sequence는 결국 또 하나의 가변 인자 템플릿에 불과하므로 생성할 때 여전히 0, 1, 2, 3과 같은 인덱스를 템플릿 인자로 주어야 한다.

하지만, std::make_index_sequence{} 는 인덱싱 할 길이만 지정하면 된다.

std::make_index_sequence< n >{} 은 std::index_sequence< 0, 1, 2, 3, ..., n - 1 > 을 생성한다.

인덱스를 하나하나 지정해서 생성할 필요가 없어진다!

 

 

#include <iostream>
 
using namespace std;
 
template < size_t ... Idx, typename ... Args >
void increment( index_sequence<Idx...>, Args&... args )
{
    int dummy[] = { (args += Idx)... };
}
 
int main()
{
    int a = 1;
    double b = 1.0;
    long long c = 1;
 
    increment( make_index_sequence<3>{}, a, b, c );
 
    cout << a << ", " << b << ", " << c << endl;
    // output : 1, 2, 3
}
cs

 

 

pack expansion 을 위해서 더미 배열을 생성하였다.

현재 increment 의 템플릿 인자로 쓰인 자료형들은 모두 int 로의 암시적 형변환이 가능하므로 해당 표현에 문제가 없다.

이후에 보겠지만, int 로의 암시적 형변환이 불가능한 경우에도 객체와 int 값 0을 짝지어 초기화 목록에 나열하면

둘 중 int 인 값으로 요소가 초기화되므로 문제없이 이용이 가능하다.

더미 배열은 가변 인자 템플릿에서 많이 이용되는 패턴이다.

C++17 에서는 fold expression 이 추가되면서 이런 더미 배열을 이용할 필요가 없어졌다.

 

 


istream 으로부터 tuple 입력받기

이제 모든 준비가 갖추어졌다.

std::get 에 쓰일 인덱스를 컴파일 타임 상수로 생성한다.

그리고 그 인덱스는 std::make_index_sequence 를 통해 만들 수 있다.

std::make_index_sequence 에 필요한 튜플의 길이는 std::tuple_size 를 이용한다.

 

std::make_index_sequence 는 인덱싱할 길이만 지정하면 되므로 sizeof... 를 통해 직접 길이를 넣을 수도 있다.

sizeof... 는 가변인자의 길이를 나타낸다.

 

 

#include <tuple>

using
 namespace std;
 
template < typename ... Elems, size_t ... Idx >
istream& istreamget_impl( istream& is, tuple< Elems... >& tup, const index_sequence< Idx... > )
{
    int dummy[ sizeof...( Elems ) ] = { ( is >> get< Idx >( tup ), 0 )... };
    return is;
}
 
template < typename ... Elems >
istream& operator>>( istream& is, tuple< Elems... >& tup )
{
    istreamget_impl( is, tup, make_index_sequence< sizeof...( Elems ) >() );
    return is;
}
cs

 

 

더미 int 배열의 초기화 목록에서 요소를 하나하나 초기화 할 때, ( ) 안에 int 값과 어떠한 다른 값을 짝지어 넣으면 둘 중에 int 인 값을 골라 초기화한다.

is >> get< idx >( tup ) 의 결과는 std::istream 의 값이므로, 이 값 대신 int 인 0이 초기화에 쓰인다.

 

 


ostream 에 튜플 출력하기

입력의 경우와 똑같이 짜되 연산자의 방향이 바뀌기만 하면 된다.

 

 

#include <tuple>

using
 namespace std;
 
template < typename ... Elems, size_t ... Idx >
ostream& ostreamput_impl( ostream& os, tuple< Elems... >& tup, const index_sequence< Idx... > )
{
    int dummy[ sizeof...( Elems ) ] = { ( os << get< Idx >( tup ) << ' '0 )...};
    return os;
}
 
template < typename ... Elems >
ostream& operator<<( ostream& os, tuple< Elems... >& tup )
{
    ostreamput_impl( os, tup, make_index_sequence< sizeof...( Elems ) >() );
    return os;
}
cs

 

 


실행

#include <iostream>
#include <tuple>
#include <sstream>
 
using namespace std;
 
template < typename ... Elems, size_t ... Idx >
istream& istreamget_impl( istream& is, tuple< Elems... >& tup, const index_sequence< Idx... > )
{
    int dummy[ sizeof...( Elems ) ] = { ( is >> get< Idx >( tup ), 0 )... };
    return is;
}
 
template < typename ... Elems >
istream& operator>>( istream& is, tuple< Elems... >& tup )
{
    istreamget_impl( is, tup, make_index_sequence< sizeof...( Elems ) >() );
    return is;
}
 
template < typename ... Elems, size_t ... Idx >
ostream& ostreamput_impl( ostream& os, tuple< Elems... >& tup, const index_sequence< Idx... > )
{
    int dummy[ sizeof...( Elems ) ] = { ( os << get< Idx >( tup ) << ' '0 )...};
    return os;
}
 
template < typename ... Elems >
ostream& operator<<( ostream& os, tuple< Elems... >& tup )
{
    ostreamput_impl( os, tup, make_index_sequence< sizeof...( Elems ) >() );
    return os;
}
 
int main()
{
    stringstream ss;
    tuple<intdoublestring> tup;
    ss << "48 85 너지?\n";
    ss >> tup;
    cout << tup;
    // output : 48 85 너지?
}
cs