본문 바로가기
프로그래밍/C & C++

[C++] Pairs and Tuples

by 별준 2022. 12. 6.

References

  • The C++ Standard Library 2nd (Ch 5.1)

Contents

  • std::pair
  • std::tuple

아래 내용은 C++11 기준으로 작성된 내용입니다.

C++98의 첫 번째 C++ 표준 라이브러리에서는 특정 클래스를 정의하지 않고 다른 타입의 value pairs를 처리하는 간단한 클래스가 제공되었습니다. 이 C++98 클래스는 표준 함수로부터 value pair를 리턴할 때나 컨테이너의 요소가 key/value pairs일 때 사용되었습니다.

 

TR1에서는 요소의 수가 제한되었지만, 임의의 수의 요소를 가질 수 있도록 pair의 개념을 확장한 tuple 클래스를 도입했습니다. 여기서는 최대 10개의 다른 타입의 요소를 가질 수 있는 tuple이 구현되었습니다.

 

C++11부터 tuple 클래스는 가변 템플릿(variadic templates) 개념을 사용하여 새로 구현되었습니다. 이때부터, 표준 tuple 클래스는 요소의 갯수를 제한받지 않습니다. 또한, 여전히 2개의 요소를 갖는 pair 클래스도 제공되며, 2개의 요소의 tuple과 결합하여 사용할 수 있습니다.

 

그러나 C++11에서 pair 클래스 또한 많이 확장되었는데, 이는 C++이 언어나 라이브러리 측면에서 개선이 된 점들을 잘 보여주는 예 입니다.

 

Pairs

pair 클래스는 두 개의 값을 하나의 단위로 취급합니다. 이 클래스는 C++ 표준 라이브러리의 여러 군데서 사용되는데, 특히 컨테이너 클래스인 map, multimap, unordered_map, unordered_multimap에서 요소들을 key/value 쌍으로 관리하기 위해 pair 클래스를 사용합니다. 또 다른 예시로는 minmax() 와 같이 두 개의 값을 리턴하는 함수에서 pair가 사용됩니다.

 

pair 구조는 <utility>에 정의되어 있으며 아래의 연산들을 제공합니다. 원칙적으로 생성한 뒤, 복사/할당/교환 그리고 비교할 수 있습니다. 또한, 첫 번째와 두 번째 값의 데이터 타입을 나타내는 first_type, second_type이라는 데이터 타입 정의도 같이 제공하고 있습니다.

Element Access

pair의 값들을 처리하기 위해 대응하는 멤버에 직접 액세스할 수 있습니다. 사실 pair는 클래스가 아닌 구조체(struct)로 선언되어 있기 때문에 모든 멤버가 public 입니다.

<stl_pair.h> 일부

예를 들어, 스트림에 value pair를 쓰는 제너릭 함수 템플릿 구현은 다음과 같이 작성하면 됩니다.

// generic output operator for pairs (limited solution)
template<typename T1, typename T2>
std::ostream& operator<<(std::ostream& strm, const std::pair<T1, T2>& p)
{
  return strm << "[" << p.first << "," << p.second << "]";
}

 

또한, C++11부터는 tuple과 유사한 인터페이스도 사용할 수 있습니다. 따라서, tuple_size<>::value를 사용하여 요소의 갯수를 얻을 수 있고 tuple_element<>::type을 사용하여 지정된 요소의 타입을 얻을 수도 있으며, get()을 사용하여 first 또는 second에 액세스할 수 있습니다.

typedef std::pair<int,float> IntFloatPair;
IntFloatPair p(42, 3.14);

std::get<0>(p); // yield p.first
std::get<1>(p); // yield p.second
std::tuple_size<IntFloatPair>::value // yield 2
std::tuple_element<0, IntFloatPair>::type // yield int

 

Constructors and Assignment Operators

기본 생성자들은 요소들의 타입의 기본 생성자로 초기화된 값으로 value pair를 생성합니다. 언어의 규칙 때문에, 기본 생성자를 명시적으로 호출해도 int와 같이 기본 데이터 타입들 역시 초기화됩니다.

std::pair<int,float> p; // initialize p.first and p.second with zero

따라서, 위의 선언은 p의 값들을 int()와 float()를 사용하여 초기화하고, 둘 다 모두 0으로 초기화됩니다.

 

동일한 타입의 pair에 대해서는 복사 생성자(copy constructor)와 멤버 템플릿(member template)이 모두 제공되는데, 멤버 템플릿은 implicit type conversion이 필요할 때 사용됩니다. 만약 타입이 일치한다면, 암묵적으로 생성된 복사 생성자가 호출됩니다. 예를 들면 다음과 같습니다.

void f(std::pair<int, const char*>);
void g(std::pair<const int, std::string>);
...

void foo() {
  std::pair<int, const char*> p(42, "hello");
  f(p); // OK, call implictly generated copy constructor
  g(p); // OK, call template constructor
}

 

C++11부터는 nonconstant copy constructor는 더 이상 컴파일되지 않습니다.

class A
{
public:
  ...
  A(&A); // copy constructor with nonconstant reference
  ...
};

std::pair<A, int> p; // Error since C++11

 

C++11부터는 할당 연산자(assignment operator) 또한 멤버 템플릿으로 제공되기 때문에 implicit type conversion이 가능하고, move semantics도 지원됩니다.

 

Piecewise Construction

pair<> 클래스는 초기값으로 first와 second를 초기화하는 3가지 생성자를 제공합니다.

namespace std {
  template<typename T1, typename T2>
  struct pair {
    ...
    pair<const T1& x, const T2& y);
    template<typename U, typename V> pair(U&& x, V&& y);
    template<typename... Args1, typename... Args2>
      pair(piecewise_construct_t,
           tuple<Args1...> first_args,
           tuple<Args2...> second_args);
    ...
  };
}

위의 처음 두 생성자는 일반적인 방식으로 동작합니다. first에 인자를 하나 전달하고, 나머지 인자는 second에 전달합니다. move semantics와 implicit type conversion도 지원됩니다.

 

하지만, 마지막 생성자는 조금 다릅니다. 일반적으로 하나 또는 두 개의 tuple을 전달하면 처음 두 생성자는 pair로 초기화합니다. 하지만 세 번째 생성자는 tuple의 요소를 first와 second의 생성자로 전달하며, 이 동작을 위해서는 첫 번째 인자에 std::piecewise_construct를 전달해주어야 합니다.

#include <iostream>
#include <utility>
#include <tuple>

class Foo {
public:
  Foo(std::tuple<int, float>) {
    std::cout << "Foo::Foo(tuple)\n";
  }
  template<typename... Args>
  Foo(Args... args) {
    std::cout << "Foo::Foo(args...)\n";
  }
};

int main(int argc, char** argv)
{
  // create tuple t
  std::tuple<int, float> t(1, 2.22);

  // pass the tuple as a whole to the constructor of Foo
  std::pair<int, Foo> p1(42, t);

  // pass the elements of the tuple to the constructor of Foo
  std::pair<int, Foo> p2(std::piecewise_construct, std::make_tuple(42), t);
}

위의 코드의 결과처럼 첫 번째 인자로 piecewise_construct가 전달되었을 때만 tuple 자체가 아닌 tuple의 요소를 받는 Foo 클래스의 생성자를 사용합니다. 여기서는 가변 인자를 받는 생성자가 호출되었고, 만약 Foo(int, float)라는 생성자가 있었다면 이 생성자가 호출되었을 것입니다. 하지만 이렇게 동작하려면 두 인자가 모두 tuple이어야 하기 때문에 첫 번째 인자를 전달할 때 make_tuple(42)을 사용하여 int를 tuple로 변경하여 전달했습니다.

 

make_pair()

make_pair() 함수 템플릿을 사용하면 데이터 타입을 명시적으로 지정하지 않아도 pair를 만들 수 있습니다.

std::pair<int, char)(42, '@');
std::make_pair(42, '@');

위 구문의 결과는 동일합니다.

 

C++ 이전에는 make_pair()의 선언과 정의는 상당히 간단했는데, C++11부터는 move semantics를 처리해야 하기 때문에 다음과 같이 변경되었습니다.

#if __cplusplus >= 201103L
  template<typename _T1, typename _T2>
    constexpr pair<typename __decay_and_strip<_T1>::__type,
                   typename __decay_and_strip<_T2>::__type>
    make_pair(_T1&& __x, _T2&& __y)
    {
      typedef typename __decay_and_strip<_T1>::__type __ds_type1;
      typedef typename __decay_and_strip<_T2>::__type __ds_type2;
      typedef pair<__ds_type1, __ds_type2> 	      __pair_type;
      return __pair_type(std::forward<_T1>(__x), std::forward<_T2>(__y));
    }
#else
  template<typename _T1, typename _T2>
    inline pair<_T1, _T2>
    make_pair(_T1 __x, _T2 __y)
    { return pair<_T1, _T2>(__x, __y); }
#endif

그 결과, 반환되는 pair의 데이터 타입은 __x와 __y의 데이터 타입에 따라 결정됩니다.

 

인자로 pair를 받는 함수에 pair를 전달할 때 make_pair()를 사용하면 편리합니다.

void f(std::pair<int, const char*>);
void g(std::pair<const int, std::string>);
...

void foo() {
  f(std::make_pair(42, "empty"); // pass two values as pair
  g(std::make_pair(42, "chair"); // pass two values as pair with type conversion
}

위 코드에서 볼 수 있듯이, make_pair()는 데이터 타입이 정확하게 일치하지 않아도 동작하는데, 이는 템플릿 생성자가 암묵적으로 데이터 타입을 변환시켜주기 때문입니다.

 

또한, C++11부터는 아래와 같은 initializer list도 사용할 수 있습니다.

f({42,"empty"});
g({42,"chair"});

 

C++11에서 move semantics를 사용하려면 단순히 std::move()만 사용해주면 됩니다.

std::string s, t;
...
auto p = std::make_pair(std::move(s), std::move(t));

 

만약 reference semantics를 강제하고 싶다면, ref() (for reference type) 또는 cref() (for constant reference type)을 사용하면 됩니다. 두 함수 모두 <functional>에 정의되어 있습니다. 예를 들어, 아래의 구문에서 pair의 요소가 각각 i를 참조하기 때문에 결과적으로 i는 2가 됩니다.

#include <iostream>
#include <utility>
#include <functional>

int i = 0;
auto p = std::make_pair(std::ref(i), std::ref(i)); // create pair<int&,int&>
++p.first;  // increment i
++p.second; // increment i
std::cout << "i: " << i << std::endl; // print i: 2

 

<tuple>에 정의된 tie() 인터페이스를 사용하여 pair의 값을 추출할 수도 있는데, tie는 tuple에서 언급하도록 하겠습니다.

 

Pair Comparisons

두 개의 pair를 서로 비교할 수 있도록, C++ 표준 라이브러리는 일반적인 비교 연산자를 제공합니다. 만약 두 값이 모두 동일하다면, 두 pair는 동일합니다.

/// Two pairs of the same type are equal iff their members are equal.
template<typename _T1, typename _T2>
  inline _GLIBCXX_CONSTEXPR bool
  operator==(const pair<_T1, _T2>& __x, const pair<_T1, _T2>& __y)
  { return __x.first == __y.first && __x.second == __y.second; }

비교할 때는 첫 번째 값(first)의 우선순위가 더 높습니다. 따라서 첫 번째 값이 다르다면 두 번째 값을 비교하지 않고 첫 번째 결과만으로 전체 비교의 결과가 결정됩니다.

 

다른 비교 연산자들도 위와 비슷하게 정의되어 있습니다.

 

Examples of Pair Usage

C++ 표준 라이브러리에서 pair를 자주 사용합니다. 예를 들어, map이나 multimap은 이들의 요소를 key/value 쌍으로 관리하기 위해서 pair를 사용합니다. 또한, 표준 라이브러리 함수에서 두 개의 값을 반환할 때도 사용됩니다.

 

 

Tuple

tuple은 pair의 개념을 확장시켜, 임의의 갯수의 요소를 가질 수 있도록 하는 데이터 구조이며, TR1에서 처음 도입되었다고 합니다. 즉, tuple은 컴파일 시간에 명시되거나 추론될 수 있는 데이터 타입에 대한 요소들의 heterogeneous list 입니다.

 

TR1은 C++98을 기반으로 하기 때문에 템플릿의 인자 수가 가변적일 수가 없었습니다. 따라서 실제 구현에서는 tuple 요소의 수를 명시해주어야만 했습니다. C++11부터는 가변 템플릿이 도입되면서 tuple은 임의의 갯수의 요소를 가질 수 있게 되었으며, tuple의 선언은 다음과 같습니다.

namespace std {
  template<typename... Types>
  class tuple;
}

 

Tuple Operations

원칙적으로 tuple의 인터페이스는 매우 직관적입니다.

  • 명시적으로 tuple을 생성하거나 make_tuple을 사용하여 암묵적으로 tuple을 생성할 수 있습니다.
  • get<>() 함수 템플릿을 사용하여 각 요소에 접근할 수 있습니다.

아래 예제 코드는 tuple 인터페이스에 대한 간단한 예제입니다.

#include <iostream>
#include <tuple>
#include <complex>
#include <string>

int main(int argc, char** argv)
{
  // create a 4-element tuple
  // - elements are initialized with default value
  std::tuple<std::string, int, int, std::complex<double>> t;

  // create and initialize a tuple explicitly
  std::tuple<int, float, std::string> t1(41, 6.3, "nico");

  // "iterate" over elements
  std::cout << std::get<0>(t1) << " ";
  std::cout << std::get<1>(t1) << " ";
  std::cout << std::get<2>(t1) << " \n";

  // create tuple with make_tuple()
  auto t2 = std::make_tuple(22,44,"nico");

  // assign second value in t2 to t1
  std::get<1>(t1) = std::get<1>(t2);

  // comparison and assignment
  // - including type conversion from tuple<int, int, const char*>
  // to tuple<int, float, string>
  if (t1 < t2) { // compare value for value
    t1 = t2;
  }
}

꽤나 직관적이기 때문에 위의 코드로 많은 것들이 설명됩니다. 몇 가지 헷갈릴 수 있는 것들만 언급하도록 하겠습니다.

 

tuple의 데이터 타입은 reference일 수도 있습니다.

std::string s;
std::tuple<std::string&> t(s);

std::get<0>(t) = "hello";

 

그리고 tuple은 요소를 반복할 수 있는 일반적인 컨테이너가 아닙니다. 대신 각 개별 요소에 액세스하기 위한 멤버 템플릿을 제공합니다. 따라서, 원하는 요소의 인덱스를 컴파일 시간에 알고 있어야 하며, 런타임에 인덱스를 전달할 수는 없습니다.

int i;
std::get<i>(t1); // compile-time error: i is no compile-time value

다행인 것은 범위를 벗어난 인덱스와 같이 잘못된 인덱스 값을 사용하면 컴파일 에러가 발생합니다.

 

또한, tuple은 일반적인 복사, 할당, 비교 연산자를 제공하며, 모든 연산에서 implicit type conversion이 가능합니다. 다만, 요소의 갯수를 서로 일치해야 합니다. 이때, tuple은 모든 요소가 동일할 때만 서로 같다고 간주하며 두 tuple은 사전적 비교(lexicographical comparision)을 통해 대소를 결정합니다.

 

make_tuple() and tie()

make_tuple()을 사용하면 데이터 타입을 명시적으로 지정하지 않고 tuple을 생성할 수 있습니다. 예를 들어, 아래 표현식은 int, int, const char* 타입의 요소들로 구성된 tuple을 생성하고 초기화합니다.

std::make_tuple(22, 44, "nico");

 

<functional>에 정의된 reference_wrapper<> 함수 객체와 ref(), cref()를 사용하면 make_tuple()이 추론하는 데이터 타입에 영향을 줄 수 있습니다. 예를 들어, 아래의 표현식은 변수 또는 객체 x의 reference를 요소로 갖는 tuple을 생성합니다.

std::string s;
std::make_tuple(std::ref(s)); // yield type tuple<std::string&>, where the element refers to s

 

make_tuple()과 reference를 사용하면 tuple의 값을 추출하여 다른 변수로 전달할 수 있습니다.

std::tuple<int,float,std::string> t(77, 1.1, "more light");
int i;
float f;
std::string s;
// assign values of t to i, f and s
std::make_tuple(std::ref(i), std::ref(f), std::ref(s)) = t;

또는, tuple에서 reference를 더욱 쉽게 사용할 수 있도록 제공하는 tie() 함수를 사용할 수도 있습니다.

std::tuple<int,float,std::string> t(77, 1.1, "more light");
int i;
float f;
std::string s;
std::tie(i, f, s) = t; // assign values of t to i, f, and s

std::tie(i, f, s)는 i, f, s에 대한 reference를 가지는 tuple을 생성하며, t를 할당하면 t의 각 요소의 값이 i, f, s에 할당됩니다.

 

std::ignore를 사용하면 tie()에서 파싱할 때 특정 요소를 무시할 수 있으며, 일부 값만 추출할 때 유용합니다.

std::tuple<int, float, std::string> t(77, 1.1, "more light");
int i;
std::string s;
std::tie(i, std::ignore, s) = t; // assign first and third value of t to i and s

 

Tuples and Initializer Lists

tuple을 초기화하는데 사용되는 가변 인자를 받는 생성자는 explicit로 선언되어 있습니다.

namespace std {
  template<typename... Types>
  class tuple {
  public:
    explicit tuple(const Types&...);
    template<typename... UTypes> explicit tuple(UTypes&&...);
  };
}

이는 암묵적으로 하나의 요소를 갖는 tuple로 변환되지 않도록 하기 위함입니다.

template<typename... Args>
void foo(const std::tuple<Args...> t);

foo(42); // ERROR: explicit conversion to tuple<> required
foo(std::make_tuple(42)); // OK

하지만, 이 때문에 tuple의 값을 정의하기 위해 initializer list를 사용하는 방법이 변경되었는데, 예를 들어, tuple을 할당으로 초기화할 때 initializer list는 implicit conversion으로 간주되어 사용할 수 없습니다.

std::tuple<int,double> t1(42, 3.14); // OK, old syntax
std::tuple<int,double> t2{42, 3.14}; // OK, new syntax
std::tuple<int,double> t3 = {42, 3.14}; // ERROR

또한, tuple이 있어야 할 자리에 initializer list도 사용할 수 없습니다.

std::vector<std::tuple<int,float>> v{ {1,1.0}, {2.2.0} }; // ERROR

std::tuple<int,int,int> foo() {
  return { 1, 2, 3 }; // ERROR
}

(pair<>와 다른 컨테이너에서는 initializer list를 사용할 수 있습니다)

 

Additional Tuple Features

tuple에는 몇 가지 helper가 선언되어 있는데 제너릭 프로그래밍을 지원하는데 사용됩니다.

  • std::tuple_size<TupleType>::value - 요소의 수를 알려줌
  • std::tuple_element<idx, TupleType>::type - idx가 가리키는 요소의 데이터 타입을 나타냄
  • std::tuple_cat() - 여러 튜플을 하나의 튜플로 연결함

 

 

'프로그래밍 > C & C++' 카테고리의 다른 글

[C++] Type Traits와 Type Utilities  (0) 2022.12.07
[C++] Numeric Limits  (0) 2022.12.06
[C++] C++11에서 추가된 기능 (2)  (0) 2022.12.04
[C++] C++11에서 추가된 기능 (1)  (0) 2022.12.03
[C/C++] 동적 라이브러리  (0) 2022.11.16

댓글