무엇인가
알고리즘을 갈아 끼울 수 있게 밖으로 빼낸다. 쓰는 쪽 코드는 그대로 두고 방법만 바꾼다.
GoF 패턴 중 가장 순수하게 “메서드 하나짜리 인터페이스” 다.
그래서 가장 깔끔하게 접힌다 — 사실상 std::function 그 자체다.
구조
graph LR CTX[문맥: 정렬하는 쪽] -->|비교 방법을 물어본다| S[전략] S --> S1[오름차순] S --> S2[내림차순] S --> S3[길이순]
클래스판
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>
struct CompareStrategy {
virtual ~CompareStrategy() = default;
virtual bool less(const std::string& a, const std::string& b) const = 0;
};
struct Ascending : CompareStrategy {
bool less(const std::string& a, const std::string& b) const override { return a < b; }
};
struct Descending : CompareStrategy {
bool less(const std::string& a, const std::string& b) const override { return b < a; }
};
struct ByLength : CompareStrategy {
bool less(const std::string& a, const std::string& b) const override {
return a.size() < b.size();
}
};
class Sorter {
const CompareStrategy& strategy_;
public:
explicit Sorter(const CompareStrategy& s) : strategy_(s) {}
void sort(std::vector<std::string>& v) const {
std::sort(v.begin(), v.end(), [this](const std::string& a, const std::string& b) {
return strategy_.less(a, b);
});
}
};
void print(const std::vector<std::string>& v) {
for (const auto& s : v) std::cout << s << ' ';
std::cout << '\n';
}
int main() {
std::vector<std::string> v{"banana", "fig", "apple", "cherry"};
Ascending asc;
Descending desc;
ByLength len;
auto a = v; Sorter{asc}.sort(a); print(a);
auto b = v; Sorter{desc}.sort(b); print(b);
auto c = v; Sorter{len}.sort(c); print(c);
}클래스가 다섯이다(인터페이스 1 + 전략 3 + 문맥 1).
그리고 Sorter::sort 안에서 이미 람다를 쓰고 있다 — 표준 라이브러리가 원래 이 방식이기 때문이다.
전략 인터페이스는 그 람다를 한 겹 더 감싼 것에 지나지 않는다.
람다판
less 하나뿐인 인터페이스다. 통째로 사라진다.
#include <algorithm>
#include <functional>
#include <iostream>
#include <string>
#include <vector>
using Compare = std::function<bool(const std::string&, const std::string&)>;
void sortWith(std::vector<std::string>& v, const Compare& cmp) {
std::sort(v.begin(), v.end(), cmp);
}
void print(const std::vector<std::string>& v) {
for (const auto& s : v) std::cout << s << ' ';
std::cout << '\n';
}
int main() {
std::vector<std::string> v{"banana", "fig", "apple", "cherry"};
auto a = v;
sortWith(a, [](const std::string& x, const std::string& y) { return x < y; });
print(a);
auto b = v;
sortWith(b, [](const std::string& x, const std::string& y) { return y < x; });
print(b);
auto c = v;
sortWith(c, [](const std::string& x, const std::string& y) { return x.size() < y.size(); });
print(c);
// 전략을 즉석에서 만든다 — 클래스판이라면 새 클래스가 필요했다
const char target = 'a';
auto count = [target](const std::string& s) {
return std::count(s.begin(), s.end(), target);
};
auto d = v;
sortWith(d, [count](const std::string& x, const std::string& y) {
return count(x) > count(y);
});
print(d);
}클래스가 0이다. 그리고 마지막 정렬이 요점이다 — “특정 글자가 많은 순”이라는 전략을
그 자리에서 만들었다. 클래스판이라면 ByCharCount 클래스를 정의하고,
target을 받을 생성자를 붙이고, 인스턴스를 만들어 넘겨야 했다.
무엇이 달라졌나
| 클래스판 | 람다판 | |
|---|---|---|
| 전략 3개일 때 클래스 수 | 5 | 0 |
| 전략에 파라미터 | 멤버 + 생성자 | 캡처 |
| 일회용 전략 | 클래스를 만들어야 한다 | 그 자리에서 |
| 전략의 이름 | 타입 이름으로 남는다 | 남지 않는다 |
마지막 줄이 유일하게 클래스판이 이기는 항목이다.
Descending이라는 이름은 그 자체로 문서지만 [](auto& x, auto& y){ return y < x; }는 아니다.
뜨거운 경로라면 템플릿으로
std::function은 간접 호출이고 경우에 따라 힙을 쓴다.
정렬 비교처럼 수없이 불리는 자리라면 타입을 지우지 않는 편이 낫다.
template <typename Compare>
void sortWith(std::vector<std::string>& v, Compare cmp) {
std::sort(v.begin(), v.end(), cmp);
}std::sort가 원래 이렇게 생겼다. 비교자를 템플릿 매개변수로 받으므로 람다가
인라인될 여지가 있다. std::function으로 감싸면 그 여지가 사라진다.
전략을 저장해 둬야 할 때만 std::function을 쓰고,
호출 지점에서 바로 쓸 때는 템플릿으로 두는 것이 기본이다.
언제 접고 언제 접지 않나
접는다 — 거의 항상. 이 패턴은 C++에서 사실상 람다로 대체됐다.
접지 않는다
- 전략이 여러 메서드를 묶어야 할 때(예:
encode/decode쌍). - 전략에 이름이 필요할 때. 설정 파일이나 UI에서 골라야 한다면
std::map<std::string, Compare>처럼 이름과 람다를 따로 묶는 방법이 있다. - 전략이 무거운 상태를 들고 여러 곳에서 공유돼야 할 때.