본문 바로가기
Topos Uranos

요약

지금까지 강좌의 컬렉션은 모두 한 가지 자료형의 데이터만 담았다. 성적, 키, 표의 칸이 그랬다. 그러나 문제에 나오는 개체는 자료형이 서로 다른 여러 특성을 지닌다. 학생에게는 이름과 성적이 있고, 책에는 제목과 저자와 출판 연도가 있다. 이 수업은 레코드를 소개한다. 레코드는 그런 특성들을 하나의 이름 아래 묶기 위해 프로그래머가 직접 struct 키워드로 정의하는 복합 자료형이다. 수업은 레코드가 존재하는 이유, 곧 병렬 벡터의 취약함에서 출발하여, struct 키워드의 정의, 초깃값을 가진 필드, 중괄호와 지정 초기화자를 이용한 레코드 생성으로 이어진다. 이어서 함수의 매개변수와 결과로서의 레코드를 다루고, 레코드의 벡터를 다룬다. 레코드의 벡터는 구조적 바인딩으로 순회하고 한 필드를 기준으로 정렬하고 탐색하며, 여기서 안정 정렬의 문제도 함께 살핀다. 끝으로 문제의 영역을 반영하는 자료형으로 문제를 모델링하는 기술로 마무리한다.

학습 목표

  1. 필드와 초깃값을 가진 struct 타입을 정의하고, 중괄호 초기화로, 지정 초기화자도 써서 레코드를 만든다.
  2. 전달 방식을 골라 레코드를 받고 반환하는 함수를 작성한다.
  3. 프로젝션을 곁들인 std::ranges::sort, std::ranges::stable_sort, std::ranges::find 함수로 레코드의 벡터를 한 필드를 기준으로 정렬하고 탐색하며, 안정 정렬과 그렇지 않은 정렬을 구별한다.
  4. 문제의 데이터를 레코드로 모델링하고, 어떤 필드와 자료형을 고르는지 정당화한다.

데이터 묶기

병렬 벡터

어떤 프로그램이 한 반의 학생마다 이름, 성적, 나이를 저장해야 한다고 하자. 표 수업까지 배운 것으로 손쉽게 만드는 해법은 특성마다 하나씩 세 개의 벡터를 두고, 인덱스 ii의 값이 세 벡터에서 같은 사람을 가리킨다는 약속을 정하는 것이다. names[i], grades[i], ages[i] 원소가 그것이다. 이런 벡터를 병렬 벡터라 한다. 표는 쓸 수 없다. 표의 칸은 모두 같은 자료형이어야 하는데, 이름은 수가 아니기 때문이다.

C++ 프로그램
#include <algorithm>#include <cstddef>#include <print>#include <string>#include <vector>​int main(){    const std::vector<std::string> names{"아나", "루이스", "에바"};    std::vector<int> grades{6, 4, 7};    const std::vector<int> ages{19, 22, 20};    std::ranges::sort(grades);    for (std::size_t i{0}; i < names.size(); ++i) {        std::println("{}: 성적 {}, {}세", names[i], grades[i], ages[i]);    }}
출력
아나: 성적 4, 19세루이스: 성적 6, 22세에바: 성적 7, 20세

이 프로그램은 학생들을 성적순으로 정렬하려 했지만, 성적만 정렬했다. 경고 없이 컴파일되고, 메모리 오류 새니타이저인 AddressSanitizer도, 정의되지 않은 동작의 새니타이저인 UndefinedBehaviorSanitizer도 보고할 것이 없다. 그런데도 출력은 틀렸다. 아나의 성적은 6이었는데 4로 나온다. 세 벡터를 묶던 약속은 컴파일러가 읽을 수 있는 곳 어디에도 적혀 있지 않으므로, 그 약속이 깨지는 것을 막는 것은 아무것도 없다. 여기서 병렬 벡터의 취약함이 나온다. 원소를 옮기거나 추가하거나 제거하는 연산은 모두 모든 벡터에서 똑같이, 같은 순서로 되풀이해야 한다. 그리고 하나만 잊어도 데이터는 소리 없이 망가진다.

예시어긋나는 행과 통째로 움직이는 카드

세 개의 병렬 벡터와, 한 사람의 세 데이터를 각각 담은 카드의 벡터를 비교한다.

증명

  1. 열 0은 한 사람을 나타낸다. names[0], grades[0], ages[0] 원소는 아나의 이름, 성적, 나이이다. 이들을 잇는 것은 인덱스뿐이다.

  2. std::ranges::sort(grades) 호출은 한 행만 다시 정렬한다. 나머지 두 행은 그 사실을 모른다. 이제 열 0은 아나의 이름, 루이스의 성적, 아나의 나이를 한데 모으고 있으며, 이것은 누구의 것도 아니다.

  3. 대안은 원소가 카드인 벡터 하나만 쓰는 것이다. 카드마다 한 사람의 세 데이터가 들어 있고, 이들을 잇는 것은 더 이상 인덱스에 관한 약속이 아니라 카드 자체이다.

  4. 성적으로 정렬하면 카드가 통째로 움직인다. 루이스의 카드는 이름과 나이를 지닌 채 첫 자리로 옮겨 간다. 어떤 데이터도 나머지와 떨어질 수 없다. 연산이 그것들을 따로따로 보지 않기 때문이다.

그렇다면 문제가 요구하는 것은 값이 카드인 자료형이다. 곧 이름, 성적, 나이를 담고, 하나의 전체로서 복사되고, 함수에 전달되고, 벡터에 저장되고, 정렬되는 값이다. C++에는 그런 자료형이 미리 만들어져 있지 않다. 문제마다 카드가 다르기 때문이다. 그러나 프로그래머가 그것을 정의하는 것은 허용한다. 그 자료형이 레코드이다.