Zero-Overhead Abstractions: Building Vector Math Libraries with Concepts and Customization Points
개요
과학 컴퓨팅(Scientific Computing) 분야에서 제네릭(Generic) 벡터 연산 라이브러리를 작성하는 것은 오랜 과제였다. 1990년대부터 객체지향 프로그래밍(Object-Oriented Programming)을 통해 이를 구현하려는 시도가 있었지만, 성능(Performance)과 추상화(Abstraction) 사이의 트레이드오프를 극복하지 못했다. 이 강연은 C++11 이후, 특히 C++20의 Concepts와 Customization Point Objects(CPO)를 활용하여 이러한 문제를 해결하는 방법을 제시한다.
벡터 연산의 중요성과 설계 과제
벡터 연산은 AI/ML, 과학 계산, PDE 제약 최적화(PDE-constrained Optimization), 게임 그래픽, 이미지 처리 등 거의 모든 기술 분야에 필수적이다. 이상적인 벡터 수학 라이브러리는 다음 조건을 만족해야 한다.
- 타입 독립성(Type Independence): 임의의 벡터 타입에서 작동
- 헤더 독립성(Header Independence): 특정 타입의 헤더에 의존하지 않음
- 제로 오버헤드(Zero Overhead): 추상화로 인한 런타임 비용 없음
- 확장 가능성(Extensibility): 새로운 타입 추가 용이
그러나 전통적인 접근법들은 각각 한계가 있었다.
전통적 접근법의 한계
객체지향 프로그래밍(OOP)의 문제점
강연자가 Sandia 국립연구소에서 11년간 개발해온 Rapid Optimization Library(ROL)는 초기에 객체지향 방식으로 설계되었다. 순수 가상 함수(Pure Virtual Function)로 인터페이스를 정의하는 방식이다.
class Vector {
public:
virtual void add_in_place(const Vector& other) = 0;
virtual void scale_in_place(double alpha) = 0;
virtual double inner_product(const Vector& other) const = 0;
virtual size_t dimension() const = 0;
virtual std::unique_ptr<Vector> clone() const = 0;
};이 접근법의 문제점:
-
Clone 메서드의 제약:
clone()이unique_ptr<Vector>를 반환하도록 강제되면, 스택 할당(Stack Allocation), 메모리 아레나(Memory Arena), 뷰(View) 등 다양한 메모리 관리 전략을 사용할 수 없다. -
인터페이스 팽창(Interface Bloating): Conjugate Gradient 같은 알고리즘을 구현하다 보면
axpby()(alphax + betay) 같은 복합 연산이 필요해진다. 이를 기본 클래스에 추가하면 슬리퍼리 슬로프(Slippery Slope)에 빠진다. -
GPU 호환성: GPU에서 가상 함수 룩업(Virtual Function Lookup)은 권장되지 않는다.
-
유지보수성: 실제 프로젝트에서 1,000줄 이상의 객체 클래스를 본 적이 있는가? 10,000줄은? 이는 인간의 인지 한계를 넘어선다.
CRTP와 Traits의 제약
template<typename Derived>
class VectorCRTP {
public:
void add_in_place(const VectorCRTP& other) {
static_cast<Derived*>(this)->add_in_place_impl(
static_cast<const Derived&>(other)
);
}
};CRTP(Curiously Recurring Template Pattern)와 Traits는 다음 문제가 있다.
- 완전한 제네릭성 부족: Concepts를 적용할 수 없고, 특정 타입에 대해 특수화(Specialization)해야 한다.
- “모든 벡터 타입에 대해”라는 표현 불가: 알고리즘이 필요로 하는 연산을 구현하는 모든 것에 대해 정의할 수 없다.
Free Function Overloads와 ADL 문제
자유 함수(Free Function) 오버로딩은 또 다른 문제를 일으킨다: Argument-Dependent Lookup(ADL). ADL은 연산자 오버로딩의 문법적 편의성을 위해 도입되었지만, 예상치 못한 곳에서 함수를 찾아 충돌을 일으킬 수 있다.
C++20 Concepts: 요구사항의 명시적 표현
Concepts 기반 인터페이스 정의
5가지 핵심 연산을 Concepts로 표현할 수 있다.
// 1. 벡터 덧셈
template<typename T>
concept AddInPlace_C = requires(T& v, const T& w) {
{ add_in_place(v, w) } -> std::same_as<void>;
};
// 2. 스칼라 곱셈
template<typename T, typename S>
concept ScaleInPlace_C = requires(T& v, S alpha) {
{ scale_in_place(v, alpha) } -> std::same_as<void>;
};
// 3. 내적
template<typename T>
concept InnerProduct_C = requires(const T& v, const T& w) {
{ inner_product(v, w) } -> RealScalar_C;
};
// 4. 차원
template<typename T>
concept Dimension_C = requires(const T& v) {
{ dimension(v) } -> std::integral;
};
// 5. 복제
template<typename T>
concept Clone_C = requires(const T& v) {
{ deref_if_needed(clone(v)) } -> std::convertible_to<T>;
};통합된 Real Vector Concept
template<typename T>
concept RealVector_C =
AddInPlace_C<T> &&
Clone_C<T> &&
Dimension_C<T> &&
InnerProduct_C<T> &&
ScaleInPlace_C<T, std::invoke_result_t<decltype(inner_product), T, T>>;이 정의는 “스칼라 타입은 내적이 반환하는 타입”이라는 관계를 명시적으로 표현한다.
Conjugate Gradient 알고리즘 구현
이제 알고리즘을 수학적 표기에 가깝게 작성할 수 있다.
template<RealVector_C V, typename LinearOperator>
requires SelfMap_C<LinearOperator, V>
void conjugate_gradient(
V& x,
const LinearOperator& A,
const V& b,
double tolerance,
size_t max_iterations
) {
auto r_cl = clone(b);
auto& r = deref_if_needed(r_cl);
// r = b - A*x
A(r, x);
scale_in_place(r, -1.0);
add_in_place(r, b);
auto p_cl = clone(r);
auto& p = deref_if_needed(p_cl);
double rs_old = inner_product(r, r);
for(size_t iter = 0; iter < max_iterations; ++iter) {
if(std::sqrt(rs_old) < tolerance) break;
auto Ap_cl = clone(p);
auto& Ap = deref_if_needed(Ap_cl);
A(Ap, p);
double alpha = rs_old / inner_product(p, Ap);
// x += alpha * p
scale_in_place(p, alpha);
add_in_place(x, p);
// r -= alpha * Ap
scale_in_place(Ap, -alpha);
add_in_place(r, Ap);
double rs_new = inner_product(r, r);
// p = r + (rs_new/rs_old) * p
scale_in_place(p, rs_new / rs_old);
add_in_place(p, r);
rs_old = rs_new;
}
}deref_if_needed의 역할: clone()이 포인터를 반환하면 역참조하고, 값을 반환하면 그대로 전달한다. 이를 통해 메모리 관리 전략에 무관한 uniform 코드를 작성할 수 있다.
template<typename T>
decltype(auto) deref_if_needed(T&& t) {
if constexpr(requires { *t; }) {
return *std::forward<T>(t);
} else {
return std::forward<T>(t);
}
}Customization Point Objects: ADL 문제 해결
ADL(Argument-Dependent Lookup) 메커니즘
컴파일러가 unqualified 함수 호출을 찾는 두 가지 방법이 있다.
1. Ordinary Lookup: 로컬 스코프에서 시작해 바깥쪽으로 확장하며 첫 번째 매칭을 찾으면 중단
2. ADL: 인자 타입과 연관된 네임스페이스에서 함수를 찾는다.
namespace mylib {
template<typename T>
struct MyVector { /*...*/ };
template<typename T>
void add_in_place(MyVector<T>& v, const MyVector<T>& w) {
// 구현
}
}
// ADL이 찾을 수 있음
mylib::MyVector<double> v, w;
add_in_place(v, w); // mylib::add_in_place 호출ADL이 찾는 것:
- 타입이 정의된 네임스페이스의 함수
- Friend 함수
- 템플릿 인자와 연관된 네임스페이스의 함수
ADL이 찾지 못하는 것:
- 무관한 네임스페이스의 함수
- 전역 네임스페이스 함수 (타입이 전역 네임스페이스가 아닐 때)
using선언으로만 접근 가능한 함수- Functors (함수 객체)
Customization Point Objects (CPO)
CPO는 “함수처럼 보이고 작동하지만 함수가 아닌” 객체다. ADL의 복잡성을 우회한다.
// 전통적 CPO
namespace detail {
struct AddInPlace_FTR {
template<typename T>
void operator()(T& v, const T& w) const {
add_in_place(v, w); // ADL로 찾음
}
};
}
inline constexpr detail::AddInPlace_FTR add_in_place;이제 add_in_place는 함수가 아닌 객체이므로 ADL이 적용되지 않는다. 대신 functor 내부에서 명시적으로 ADL을 사용한다.
Tag Invoke: CPO의 중앙 집중화
모든 customization을 단일 tag_invoke 함수로 통합하는 패턴이다.
namespace mylib {
template<typename T>
struct MyVector { /*...*/ };
// 단일 extension point
template<typename T>
void tag_invoke(
decltype(add_in_place), // CPO 타입을 태그로 사용
MyVector<T>& v,
const MyVector<T>& w
) {
// 구현
}
}장점:
- 네임스페이스 정리: 단일 확장 지점
- 더 나은 진단: 에러가 발생한 정확한 위치 표시
- 제로 오버헤드: 참조로 전달 시 추가 비용 없음
단점:
- 보일러플레이트 코드: 각 CPO마다 상당량의 plumbing 코드 필요
TinCup: CPO 코드 생성 도구
강연자가 개발한 도구로, tag invoke CPO의 보일러플레이트를 자동 생성한다.
기본 사용법
커맨드라인:
tin-cup --cpo '{"name": "add_in_place", "args": ["$T&", "const $T&"]}'Vim 플러그인:
:CPO add_in_place $T& const $T&VS Code/CLion: 동일한 플러그인 제공
생성되는 코드
namespace detail {
// CPO functor
struct AddInPlace_FTR : CPO_Base<AddInPlace_FTR> {
template<typename T>
constexpr void operator()(T& v, const T& w) const
noexcept(/* noexcept 검사 */)
requires requires { tag_invoke(*this, v, w); }
{
return tag_invoke(*this, v, w);
}
// Fallback - 향상된 에러 진단
using CPO_Base::operator();
};
}
inline constexpr detail::AddInPlace_FTR add_in_place;
// Concept 자동 생성
template<typename T>
concept AddInPlace_C = requires(T& v, const T& w) {
{ tag_invoke(add_in_place, v, w) } -> std::same_as<void>;
};
// 타입 trait 생성
template<typename T>
using AddInPlaceReturnType =
std::invoke_result_t<decltype(add_in_place), T&, const T&>;향상된 진단 (Enhanced Diagnostics)
CPO_Base 클래스는 일반적 실수를 감지하여 명확한 에러 메시지를 제공한다.
template<typename Derived>
class CPO_Diagnostics {
template<typename... Args>
void operator()(Args&&... args) const {
using CPO = Derived;
// 1. 포인터를 역참조해야 하는 경우
if constexpr(requires {
tag_invoke(CPO{}, *args...);
}) {
static_assert(false,
"Arguments should be dereferenced. "
"Did you forget to use deref_if_needed?");
}
// 2. const/non-const 불일치
else if constexpr(requires {
tag_invoke(CPO{}, const_cast</*...*/>(args)...);
}) {
static_assert(false,
"Argument is const but signature expects non-const");
}
// 3. 인자 순서 오류 (2개 인자의 경우)
else if constexpr(sizeof...(Args) == 2 && requires {
tag_invoke(CPO{}, /* reversed args */);
}) {
static_assert(false,
"Arguments are in wrong order");
}
// 4. 인자 개수 오류
else {
static_assert(false,
"Wrong number of arguments or incompatible types");
}
}
};실제 에러 메시지 예시:
std::unique_ptr<MyVector<double>> v_ptr = /*...*/;
normalize(v_ptr); // 에러!
// 컴파일러 출력:
// Error: No valid tag_invoke overload found for CPO 'normalize'
// Note: There is a valid overload for the dereferenced argument.
// Did you forget to dereference your argument?수천 줄의 템플릿 에러 대신 명확한 가이드를 제공한다.
Real Vector Framework: 완전한 벡터 연산 라이브러리
TinCup을 사용하여 구축한 프로덕션급 라이브러리다.
핵심 연산
// 5가지 필수 연산
add_in_place(v, w); // v += w
scale_in_place(v, alpha); // v *= alpha
auto s = inner_product(v, w); // <v, w>
auto n = dimension(v); // dim(v)
auto v_new = clone(v); // 복제확장 연산
// AXPY 변형
axpy(v, alpha, w); // v += alpha * w
axpby(v, alpha, w, beta); // v = alpha*v + beta*w
// 원소별 연산 (Element-wise Operations)
unary_in_place(v, [](auto x) { return std::sin(x); });
binary_in_place(v, w, [](auto x, auto y) { return x * y; });
// AI/ML 연산
relu_in_place(v); // max(0, x)
softmax_in_place(v); // exp(x_i) / sum(exp(x_j))
// Variadic 연산
variadic_transform(result, v1, v2, v3,
[](auto x, auto y, auto z) { return x + y * z; });메모리 관리 최적화
문제: clone()을 반복 호출하면 메모리 할당/해제 오버헤드가 크다.
해결책: Memory Arena 패턴
template<typename T>
class ManagedVector {
std::vector<T> data_;
MemoryArena* arena_; // Observer pointer
public:
// tag_invoke for clone
friend ManagedVector tag_invoke(
decltype(clone),
const ManagedVector& v
) {
if(v.arena_) {
return v.arena_->get_or_create<ManagedVector>(v.dimension());
}
return ManagedVector(v.dimension());
}
};
class MemoryArena {
std::vector<std::unique_ptr<ManagedVector>> pool_;
std::vector<bool> in_use_;
public:
template<typename V>
V& get_or_create(size_t dim) {
// 재사용 가능한 벡터 찾기
for(size_t i = 0; i < pool_.size(); ++i) {
if(!in_use_[i] && pool_[i]->dimension() == dim) {
in_use_[i] = true;
return *pool_[i];
}
}
// 새로 생성
pool_.push_back(std::make_unique<V>(dim));
in_use_.push_back(true);
return *pool_.back();
}
};성능 개선: 벤치마크 결과 수백~수천 배 속도 향상 (문서 참조)
구현된 알고리즘
- Conjugate Gradient (CG): 대칭 양정부 선형시스템 해법
- Limited-Memory BFGS (L-BFGS): 준-뉴턴 최적화
- Projected Gradient Descent: 제약 최적화
- Truncated CG Trust Region: 신뢰 영역 기반 최적화
- Transformer Models: 몇 가지 트랜스포머 구조
std::ranges와의 통합
Range 개념을 만족하는 모든 타입에 대해 자동으로 작동한다.
// std::vector에 대한 기본 구현
namespace detail {
template<std::ranges::range R>
void tag_invoke(
decltype(add_in_place),
R& v,
const R& w
) requires std::copyable<R> {
std::transform(
std::begin(v), std::end(v),
std::begin(w),
std::begin(v),
std::plus<>{}
);
}
template<std::ranges::range R>
auto tag_invoke(
decltype(inner_product),
const R& v,
const R& w
) {
return std::inner_product(
std::begin(v), std::end(v),
std::begin(w),
typename R::value_type{0}
);
}
}이제 std::vector<double>, std::array<float, 3>, std::valarray 등에서 즉시 사용 가능하다.
std::vector<double> x = {1, 2, 3};
std::vector<double> y = {4, 5, 6};
add_in_place(x, y); // x = {5, 7, 9}
double dot = inner_product(x, y); // 5*4 + 7*5 + 9*6 = 109고급 기능
실행 정책 (Execution Policies)
TinCup은 컴파일 타임 또는 런타임 정책을 지원한다.
{
"name": "transform",
"args": ["$V&", "const $F&"],
"options": ["sequenced", "parallel", "vectorized"]
}생성되는 코드:
struct Transform_FTR : CPO_Base<Transform_FTR> {
struct Sequenced {};
struct Parallel {};
struct Vectorized {};
static constexpr Sequenced sequenced{};
static constexpr Parallel parallel{};
static constexpr Vectorized vectorized{};
// 컴파일 타임 dispatch
template<typename V, typename F>
void operator()(Sequenced, V& v, const F& f) const {
return tag_invoke(*this, sequenced, v, f);
}
// 런타임 dispatch
enum class Policy { Sequenced, Parallel, Vectorized };
template<typename V, typename F>
void operator()(Policy p, V& v, const F& f) const {
switch(p) {
case Policy::Sequenced:
return tag_invoke(*this, sequenced, v, f);
case Policy::Parallel:
return tag_invoke(*this, parallel, v, f);
case Policy::Vectorized:
return tag_invoke(*this, vectorized, v, f);
}
}
};Variadic Transform
임의 개수의 벡터에 대한 원소별 연산:
// CPO 정의
:CPO variadic_transform $V& const $Vs&... const $F&
// 사용 예
std::vector<double> result(n);
std::vector<double> a(n), b(n), c(n);
variadic_transform(result, a, b, c,
[](double x, double y, double z) {
return x * y + z; // Fused Multiply-Add
}
);구현:
template<typename V, typename... Vs, typename F>
void tag_invoke(
decltype(variadic_transform),
V& result,
const Vs&... inputs,
const F& func
) requires (RealVector_C<V> && ... && RealVector_C<Vs>) {
assert(((dimension(result) == dimension(inputs)) && ...));
auto it_result = std::begin(result);
auto tuple_iters = std::make_tuple(std::begin(inputs)...);
for(size_t i = 0; i < dimension(result); ++i) {
*it_result = std::apply(func,
std::make_tuple(*std::get<Is>(tuple_iters)...)
);
++it_result;
((++std::get<Is>(tuple_iters)), ...);
}
}C Math 함수의 제네릭화
<cmath>의 함수들을 임의 정밀도 타입으로 확장:
// float, double, long double 외의 타입 지원
inline constexpr auto sqrt = /* CPO */;
inline constexpr auto sin = /* CPO */;
inline constexpr auto exp = /* CPO */;
// MPFR (Multiple Precision Floating-Point Reliable) 라이브러리와 함께 사용
namespace mpfr {
mpfr_t tag_invoke(decltype(sqrt), const mpfr_t& x) {
mpfr_t result;
mpfr_init2(result, mpfr_get_prec(x));
mpfr_sqrt(result, x, MPFR_RNDN);
return result;
}
}향후 계획 및 표준화
1. Real Vector Framework 확장
- 더 많은 알고리즘 추가
- ROL (Rapid Optimization Library) 전면 재작성
- 객체지향 → CPO/tag_invoke 기반
- GPU 지원 개선
- 메모리 관리 최적화
2. std::linalg 통합
C++26의 std::linalg와의 호환성 연구:
// 가능한 미래 구현
std::linalg::matrix<double> A = /*...*/;
std::linalg::vector<double> x = /*...*/;
// RealVector_C를 만족하도록 tag_invoke 제공
auto result = conjugate_gradient(x, A, b, tol, max_iter);3. Clang Extension: 네이티브 CPO 지원
제안하는 문법:
// 가상 키워드 제안
customizable void add_in_place(auto& v, const auto& w);
// 사용자 정의
namespace mylib {
struct MyVector { /*...*/ };
customize void add_in_place(MyVector& v, const MyVector& w) {
// 구현
}
}이를 통해:
- 보일러플레이트 완전 제거
- 컴파일러 최적화 기회 증가
- 더 나은 에러 메시지
- C++29 표준 제안 목표
4. Reflection 통합
C++26 Reflection과의 결합:
// 미래 가능성
template<typename T>
void auto_generate_vector_ops() {
for_each_member<T>([](auto member) {
// 자동으로 tag_invoke 생성
generate_tag_invoke_for(member);
});
}설계 원칙 및 Best Practices
언제 객체지향을 사용할 것인가?
사용하지 말아야 할 때:
- 타입이 컴파일 타임에 결정되는 경우
- “이 객체는 무엇인가?”라는 질문의 답이 항상 같을 때
- 단순히 인터페이스를 정의하기 위해
사용해야 할 때:
- 런타임에 타입 결정이 필요한 경우
- 진정한 다형성(Polymorphism)이 필요할 때
- 플러그인 아키텍처
TinCup 사용 가이드
권장 워크플로우:
- 필요한 CPO 식별
- TinCup으로 코드 생성
- 생성된 코드를 수정하지 않음
tag_invoke구현만 작성- 표준 변경 시 재생성
# 프로젝트 초기화
tin-cup init my_vector_lib
# CPO 추가
tin-cup add --cpo '{"name": "add", "args": ["$T&", "const $T&"]}'
# 모든 CPO 재생성 (표준 업데이트 후)
tin-cup regenerate성능 고려사항
제로 오버헤드 검증:
// 벤치마크 예시
void benchmark_cpo() {
std::vector<double> x(1'000'000), y(1'000'000);
auto start = std::chrono::high_resolution_clock::now();
// CPO 버전
add_in_place(x, y);
auto end = std::chrono::high_resolution_clock::now();
// 수동 루프 버전
start = std::chrono::high_resolution_clock::now();
for(size_t i = 0; i < x.size(); ++i) {
x[i] += y[i];
}
end = std::chrono::high_resolution_clock::now();
// 두 버전의 실행 시간이 동일해야 함 (최적화 활성화 시)
}컴파일러 최적화 확인:
-O3플래그로 컴파일- 어셈블리 출력 비교 (
-S플래그) - CPO 호출이 인라인되었는지 확인
실전 예제: 벡터 정규화 (Vector Normalization)
전체 과정을 통합한 예제:
// 1. TinCup으로 CPO 생성
// :CPO normalize const $V&
// 2. 생성된 코드 (자동)
namespace detail {
struct Normalize_FTR : CPO_Base<Normalize_FTR> {
template<typename V>
constexpr auto operator()(const V& v) const
noexcept(/* ... */)
requires requires { tag_invoke(*this, v); }
{
return tag_invoke(*this, v);
}
using CPO_Base::operator();
};
}
inline constexpr detail::Normalize_FTR normalize;
// 3. 사용자 구현
namespace mylib {
template<typename T>
struct Vector2D {
T x, y;
};
template<typename T>
Vector2D<T> tag_invoke(
decltype(normalize),
const Vector2D<T>& v
) {
T norm = std::sqrt(v.x * v.x + v.y * v.y);
return {v.x / norm, v.y / norm};
}
}
// 4. 사용
mylib::Vector2D<double> v{3.0, 4.0};
auto v_unit = normalize(v); // {0.6, 0.8}
// 5. 에러 진단 예시
std::unique_ptr<mylib::Vector2D<double>> v_ptr =
std::make_unique<mylib::Vector2D<double>>(3.0, 4.0);
auto result = normalize(v_ptr); // 컴파일 에러!
// 에러 메시지:
// No valid tag_invoke overload for 'normalize'
// Note: There is a valid overload for dereferenced argument
// Suggestion: Did you mean normalize(*v_ptr)?기술적 세부사항
Concepts의 구성적 특성 (Compositional Nature)
Concepts는 조합 가능하다:
template<typename T>
concept Additive_C = AddInPlace_C<T> && Clone_C<T>;
template<typename T>
concept Metric_C = InnerProduct_C<T> && Dimension_C<T>;
template<typename T>
concept RealVector_C = Additive_C<T> && Metric_C<T> && ScaleInPlace_C<T>;
// 더 구체적인 요구사항
template<typename T>
concept NormedVector_C = RealVector_C<T> && requires(const T& v) {
{ norm(v) } -> std::convertible_to<ElementType_t<T>>;
};SFINAE에서 Concepts로의 전환
과거 (SFINAE):
template<typename T>
typename std::enable_if<
std::is_arithmetic<T>::value,
void
>::type
scale_in_place(std::vector<T>& v, T alpha) {
for(auto& elem : v) elem *= alpha;
}현재 (Concepts):
template<typename T>
requires std::is_arithmetic_v<T>
void scale_in_place(std::vector<T>& v, T alpha) {
for(auto& elem : v) elem *= alpha;
}
// 또는 더 간결하게
template<std::arithmetic T>
void scale_in_place(std::vector<T>& v, T alpha) {
for(auto& elem : v) elem *= alpha;
}Tag Invoke의 우선순위 규칙
여러 tag_invoke 오버로드가 있을 때:
- 정확한 타입 매칭 > 템플릿
- 비-템플릿 > 템플릿
- 더 특수화된 템플릿 > 일반 템플릿
namespace mylib {
struct SpecialVector : Vector { /*...*/ };
// 1순위: 정확한 타입
void tag_invoke(decltype(add_in_place),
SpecialVector& v, const SpecialVector& w) {
/* 특수 최적화 */
}
// 2순위: 템플릿
template<typename T>
void tag_invoke(decltype(add_in_place),
Vector& v, const Vector& w) {
/* 기본 구현 */
}
}비교: 표현식 템플릿 vs CPO
표현식 템플릿 (Expression Templates)
Eigen, Armadillo 등에서 사용:
// Eigen 스타일
VectorXd result = alpha * v + beta * w; // 임시 객체 생성 안 함
// 내부 구조 (단순화)
template<typename LHS, typename RHS>
struct VectorSum {
const LHS& lhs;
const RHS& rhs;
double operator[](size_t i) const {
return lhs[i] + rhs[i];
}
};
template<typename E>
struct VectorExpr {
const E& expr;
template<typename Other>
auto operator+(const VectorExpr<Other>& other) const {
return VectorExpr<VectorSum<E, Other>>{{expr, other.expr}};
}
};장점:
- 복잡한 표현식에서 임시 객체 제거
- 컴파일러 최적화 기회
단점:
- 컴파일 에러가 수천 줄
- 디버깅 어려움
- 타입 추론 복잡성
CPO 접근법
// 명시적 연산
auto temp1 = clone(v);
scale_in_place(deref_if_needed(temp1), alpha);
auto temp2 = clone(w);
scale_in_place(deref_if_needed(temp2), beta);
add_in_place(deref_if_needed(temp1), deref_if_needed(temp2));
// 또는 복합 연산 제공
axpby(result, alpha, v, beta, w); // result = alpha*v + beta*w장점:
- 명확한 에러 메시지
- 디버깅 용이
- 타입 추론 단순
단점:
- 수동 메모리 관리 (Memory Arena로 완화)
- 표현식 템플릿보다 덜 “수학적”
실무 적용 사례
PDE-Constrained Optimization
편미분방정식 제약 최적화 문제:
// 목적함수: min_u J(u, y(u))
// 제약조건: F(u, y) = 0 (PDE)
template<RealVector_C State, RealVector_C Control>
class PDEConstrainedProblem {
// 상태 방정식 해법
template<SelfMap_C<State> PDEOperator>
void solve_state(
State& y,
const Control& u,
const PDEOperator& F
) {
// Newton 방법 또는 다른 해법
// conjugate_gradient를 사용할 수 있음
}
// 수반 방정식 (Adjoint Equation)
template<LinearOperator_C<State> AdjointOp>
void solve_adjoint(
State& lambda,
const State& y,
const AdjointOp& F_adj
) {
conjugate_gradient(lambda, F_adj, /* rhs */, tol, max_iter);
}
// 기울기 계산
void compute_gradient(
Control& grad,
const State& y,
const State& lambda
) {
// ∇_u J = ∂_u L(u, y, λ)
// CPO 기반 연산 사용
}
};GPU 가속
CUDA/ROCm과의 통합:
template<typename T>
struct DeviceVector {
T* device_ptr;
size_t size_;
// 디바이스 메모리 관리
DeviceVector(size_t n) : size_(n) {
cudaMalloc(&device_ptr, n * sizeof(T));
}
~DeviceVector() {
cudaFree(device_ptr);
}
};
// tag_invoke 구현
template<typename T>
void tag_invoke(
decltype(add_in_place),
DeviceVector<T>& v,
const DeviceVector<T>& w
) {
// CUDA 커널 호출
dim3 block(256);
dim3 grid((v.size_ + 255) / 256);
add_kernel<<<grid, block>>>(v.device_ptr, w.device_ptr, v.size_);
cudaDeviceSynchronize();
}
// 동일한 알고리즘 재사용
DeviceVector<float> x_gpu(n), b_gpu(n);
auto A_gpu = /* GPU 행렬 연산자 */;
conjugate_gradient(x_gpu, A_gpu, b_gpu, tol, max_iter);
// CPU 버전과 동일한 인터페이스!관련 자료 및 참고 문헌
강연 내용과 별개로 참고할 만한 자료들:
C++ 표준 제안서
- P1895R0: “tag_invoke: A general pattern for supporting customisable functions” (Lewis Baker, Eric Niebler)
- P2300R7: “std::execution” (Eric Niebler et al.)
- P0323R12: “std::expected” (관련 에러 핸들링)
관련 라이브러리
- Eigen: https://eigen.tuxfamily.org - 표현식 템플릿 기반 선형대수
- Armadillo: http://arma.sourceforge.net - MATLAB 스타일 API
- Boost.uBLAS: https://www.boost.org/doc/libs/release/libs/numeric/ublas - Boost 선형대수
- xtensor: https://github.com/xtensor-stack/xtensor - NumPy 스타일 C++
학술 논문
- Todd Veldhuizen, “Expression Templates” (1995) - 표현식 템플릿의 원조
- Abrahams & Gurtovoy, “C++ Template Metaprogramming” (2004)
- Stroustrup, “The Design and Evolution of C++” - 언어 설계 철학
온라인 리소스
- Compiler Explorer: https://godbolt.org - 생성된 어셈블리 확인
- C++ Reference: https://en.cppreference.com - Concepts 문서
- Eric Niebler’s Blog: http://ericniebler.com - tag_invoke 창시자
- Ranges-v3: https://github.com/ericniebler/range-v3 - std::ranges의 기반
벤치마킹 도구
- Google Benchmark: https://github.com/google/benchmark
- Nanobench: https://github.com/martinus/nanobench
- Celero: https://github.com/DigitalInBlue/Celero
프로젝트 저장소
- TinCup: https://github.com/sandialabs/tin-cup
- Real Vector Framework: https://github.com/sandialabs/real-vector-framework
- ROL (Rapid Optimization Library): https://trilinos.github.io/rol.html
이들 자료는 강연에서 직접 다루지 않았지만, 주제를 더 깊이 이해하고 실무에 적용하는 데 도움이 될 것이다.
Related Reports
- Monadic Operations for Box-Like Types - Designing the Extensibility Model - Mikhail Svetkin - C++Now
- Lightning Talk - Poor Man’s Autocomplete for Template Arguments - Max Sagebaum - CppCon 2025
- Back to Basics: C++ Concepts - Jeff Garland - CppCon 2025
- When Abstractions Fix Too Much - Towards Flexible Library Design - Patrick Roberts - C++Now 2026
- A Case-study in Rewriting a Legacy Gui Library for Real-time Audio Software in Modern C++ (Reprise)