Better Expressiveness with Parallel Range Algorithms

📺 원본 영상: Lightning Talk: Better Expressiveness with Parallel Range Algorithms - Ruslan Arutyunyan 🎙️ 채널: CppCon 📅 정리 일자: 2026-04-21 ⚠️ Note: 본 리포트는 영상의 음성을 텍스트로 변환(STT)하여 정리한 것입니다. 화면에 표시된 슬라이드 및 코드는 포함되지 않을 수 있으며, 코드 예시는 강연자의 설명을 바탕으로 재구성한 것입니다.


개요

이 라이트닝 토크는 Ruslan Arutyunyan이 CppCon에서 발표한 본 강연의 마무리 부분을 독립적으로 진행한 세션이다. 핵심 주제는 단 하나의 구체적인 예제, 즉 문자열에서 단어 수를 세는 word_count 함수를 통해 C++17 방식과 Ranges 방식이 얼마나 다른 표현력을 가지는지를 대조적으로 보여주는 것이다. 강연자는 “단 두 슬라이드”로 이 차이를 명확하게 전달하겠다고 선언하며, 코드의 복잡도와 가독성 차이가 실제로 어느 정도인지를 직접적으로 체감하게 해준다.


C++17 방식 — transform_reduce의 어색함

단어 수를 세는 알고리즘의 핵심 아이디어는 인접한 두 문자를 비교하는 것이다. 구체적으로, 앞 문자가 알파벳이 아니고(!isalpha(s1)) 뒷 문자가 알파벳이면(isalpha(s2)), 그 위치가 새 단어의 시작임을 의미한다. 이 로직을 구현하려면 원본 텍스트에서 두 개의 범위가 필요한데, 하나는 마지막 문자를 제외한 범위이고 다른 하나는 첫 번째 문자를 제외한 범위다. 이 두 범위를 동시에 순회하면서 인접 쌍(pair)을 비교하는 구조다.

C++17에서 병렬로 이를 구현하기 위해 강연자는 std::transform_reduce를 std::execution::par 실행 정책(Execution Policy)과 함께 사용한다. 두 개의 이터레이터 범위를 받는 오버로드를 활용하는 방식이다. 그런데 강연자가 특히 재미있는 점으로 짚은 부분이 있다. transform_reduce의 인자 순서가 직관과 반대라는 것이다. 함수 이름은 “transform 후 reduce”이지만, 실제 인자 순서는 reduce 함수가 먼저, transform 함수가 나중에 온다. 초보자는 물론 숙련된 개발자도 헷갈리기 쉬운 인터페이스 설계다.

Transform 함수에서는 두 문자를 받아 새 단어의 시작이면 1, 아니면 0으로 변환하여 int로 반환한다. Reduce 단계에서는 이 int 값들을 모두 합산하여 최종 단어 수를 구한다. 강연자에 따르면 문자열의 경계 처리(corner case)가 추가로 필요하지만, 이 부분은 이 토크의 핵심이 아니라고 간략히 언급하고 넘어간다.

transform_reduce를 std::execution::par 실행 정책과 함께 사용하는 C++17 word_count 코드 슬라이드

01:11 - transform_reduce를 std::execution::par 실행 정책과 함께 사용하는 C++17 word_count 코드 슬라이드

아래는 강연자의 설명을 바탕으로 재구성한 C++17 버전의 개념적 코드다.

// ⚠️ 재구성된 코드 — 원본은 원본 영상 참조
// https://www.youtube.com/watch?v=UavZbFgSuUc
 
int word_count(const std::string& text) {
    if (text.empty()) return 0;
 
    // 두 범위: [begin, end-1) 과 [begin+1, end)
    // transform_reduce는 reduce 함수가 먼저, transform 함수가 나중
    int count = std::transform_reduce(
        std::execution::par,
        text.begin(), text.end() - 1,   // 첫 번째 범위 (끝에서 하나 자름)
        text.begin() + 1,               // 두 번째 범위 (시작에서 하나 자름)
        0,                              // 초기값
        std::plus<>{},                  // reduce 함수 (먼저!)
        [](char s1, char s2) -> int {   // transform 함수 (나중!)
            return (!std::isalpha(s1) && std::isalpha(s2)) ? 1 : 0;
        }
    );
 
    // corner case: 첫 번째 문자가 알파벳이면 단어 하나 추가
    return count + (std::isalpha(text.front()) ? 1 : 0);
}

transform 함수 람다 상세 설명 — !isalpha(s1) && isalpha(s2) 조건 및 int 변환 코드

02:01 - transform 함수 람다 상세 설명 — !isalpha(s1) && isalpha(s2) 조건 및 int 변환 코드


Ranges 방식 — count_if와 zip의 자연스러움

Ranges를 도입하면 동일한 알고리즘이 의도를 훨씬 명확하게 드러내는 코드로 바뀐다. 강연자는 std::ranges::count_if를 사용한다고 언급하는데, 이름 그대로 “조건을 만족하는 원소의 수를 센다”는 의미가 알고리즘 선택 자체에서 이미 드러난다. transform_reduce처럼 “변환하고 합산한다”는 우회적인 표현이 필요 없어지는 것이다.

두 개의 인접 범위를 만들기 위해서는 std::views::zip을 활용한다. 강연자의 설명에 따르면, 텍스트에 take 뷰를 파이프(|)로 연결한 것과, 텍스트에 drop 뷰를 파이프로 연결한 것을 zip으로 묶어 인접 문자 쌍의 범위를 만든다. zip이 두 범위 중 더 짧은 것에 자동으로 맞춰지므로, C++17 버전에서 수동으로 begin+1, end-1로 범위를 조정했던 번거로움이 사라진다.

count_if에 넘기는 람다(lambda)에서도 표현력의 차이가 드러난다. zip 뷰가 생성하는 원소는 두 문자에 대한 참조를 담은 튜플(tuple)이다. Ranges의 구조적 바인딩(structured binding)을 통해 이를 s1, s2로 자연스럽게 언팩(unpack)할 수 있고, 반환 타입도 억지로 int로 변환할 필요 없이 의미에 맞는 bool로 그대로 반환할 수 있다. “이 위치가 새 단어의 시작인가?”라는 질문에 true/false로 답하는 것이 1/0의 정수 합산보다 훨씬 직관적이다.

Ranges 버전 코드 슬라이드 — count_if와 zip(text|take, text|drop) 구조 전체

02:43 - Ranges 버전 코드 슬라이드 — count_if와 zip(text|take, text|drop) 구조 전체

아래는 강연자의 설명을 바탕으로 재구성한 Ranges 버전의 개념적 코드다.

// ⚠️ 재구성된 코드 — 원본은 원본 영상 참조
// https://www.youtube.com/watch?v=UavZbFgSuUc
 
int word_count(const std::string& text) {
    // zip: (text | take(n-1)) 과 (text | drop(1)) 을 묶어 인접 쌍 생성
    auto adjacent_pairs = std::views::zip(
        text | std::views::take(text.size() - 1),
        text | std::views::drop(1)
    );
 
    int count = std::ranges::count_if(
        adjacent_pairs,
        [](auto&& pair) {
            auto [s1, s2] = pair; // 구조적 바인딩으로 자연스럽게 언팩
            return !std::isalpha(s1) && std::isalpha(s2); // bool 반환
        }
    );
 
    return count + (std::isalpha(text.front()) ? 1 : 0);
}

Ranges 람다의 구조적 바인딩(s1, s2) 언팩 및 bool 반환 코드 클로즈업

03:01 - Ranges 람다의 구조적 바인딩(s1, s2) 언팩 및 bool 반환 코드 클로즈업


두 방식의 핵심 차이 비교

강연자가 이 두 슬라이드로 전달하고자 한 메시지는 단순히 “Ranges가 짧다”가 아니다. 같은 알고리즘적 의도가 코드에 얼마나 직접적으로 드러나느냐, 즉 표현력(Expressiveness)의 문제다.

비교 항목C++17 (transform_reduce)Ranges (count_if + zip)
알고리즘 선택우회적 (변환+합산으로 카운팅 구현)직접적 (count_if = 세기)
범위 구성수동으로 begin+1, end-1 조정zip + take/drop 뷰
인자 순서reduce 먼저, transform 나중 (역직관적)없음 (파이프라인으로 해결)
반환 타입int 변환 필요bool 그대로 반환
의도 명확성구현 세부사항이 노출됨의도가 코드 자체에 드러남

마무리 — 표현력이 왜 중요한가

Ruslan Arutyunyan이 이 짧은 토크에서 전달한 핵심은, 병렬 Ranges 알고리즘이 단순히 C++17 병렬 알고리즘의 “더 짧은 버전”이 아니라는 점이다. transform_reduce는 C++17 당시 병렬 카운팅을 위한 사실상 유일한 표준적 수단이었고, 그 때문에 개발자는 “단어를 세겠다”는 의도를 “변환하고 합산하겠다”는 구현으로 번역해야 했다. 그 번역 과정에서 인자 순서의 혼동, int 강제 변환, 수동 범위 조정 같은 불필요한 인지 부하가 발생한다.

Ranges는 이 번역 계층을 제거한다. count_if는 세겠다는 의미이고, zip은 묶겠다는 의미이며, 람다는 조건을 표현한다. 코드를 읽는 사람이 “이게 무엇을 하는 코드인가”를 파악하기 위해 머릿속에서 역번역을 할 필요가 없어진다. 강연자가 이 예제를 선택한 이유도 바로 여기에 있다. 기능적으로 완전히 동일한 두 코드가 독자에게 주는 인지적 부담의 차이가 이처럼 극명하기 때문이다.


📚 참고 자료 (강연 내용과 별개)

강연 내용을 더 깊이 이해하는 데 도움이 될 수 있는 표준 문서 및 관련 자료들이다. 이는 강연자가 직접 언급한 자료가 아니라, 관련 학습을 위한 추가 참고용이다.

  • std::transform_reduce (cppreference): std::execution 정책과 함께 사용하는 C++17 병렬 알고리즘
  • std::ranges::count_if (cppreference): Ranges 기반 카운팅 알고리즘
  • std::views::zip (cppreference): C++23에서 표준화된 zip 뷰 (두 범위를 쌍으로 묶음)
  • P2387 — Pipe support for user-defined range adaptors: Ranges 파이프라인 설계의 배경

📚 출처 및 참고

구분내용
원본 영상Lightning Talk: Better Expressiveness with Parallel Range Algorithms
채널CppCon
리포트 작성2026-04-21
작성 방식YouTube 영상 STT 변환 후 에디팅

💬 본 리포트는 영상 내용을 바탕으로 재구성되었습니다. 정확한 내용은 원본 영상을 참조해 주세요.