Ten wariant Strategy wybiera algorytm w czasie kompilacji za pomocą szablonów C++. Typ jest znany już przy instancjonowaniu klasy, więc kompilator generuje kod dopasowany do tego typu i nie musi przechodzić przez funkcje wirtualne.
Podejście
W tym podejściu algorytm jest parametrem szablonu:
#include <vector>
#include <algorithm>
// Interfejs Strategy (koncepcja, nie klasa abstrakcyjna)
// Każdy typ z operator() może być strategią
template<typename SortAlgorithm>
class DataProcessor {
private:
SortAlgorithm sorter;
std::vector<int> data;
public:
DataProcessor() = default;
void add_data(int value) {
data.push_back(value);
}
void process() {
sorter.sort(data);
}
void print_data() const {
for (int value : data) {
std::cout << value << " ";
}
std::cout << "\n";
}
};
// Różne strategie sortowania
struct QuickSortStrategy {
void sort(std::vector<int>& data) const {
// Implementacja quick sort
std::sort(data.begin(), data.end(),
[](int a, int b) { return a < b; });
std::cout << "Posortowane QuickSort\n";
}
};
struct MergeSortStrategy {
void sort(std::vector<int>& data) const {
// Implementacja merge sort (przy użyciu std::stable_sort)
std::stable_sort(data.begin(), data.end(),
[](int a, int b) { return a < b; });
std::cout << "Posortowane MergeSort\n";
}
};
struct HeapSortStrategy {
void sort(std::vector<int>& data) const {
// Implementacja heap sort
std::make_heap(data.begin(), data.end());
std::sort_heap(data.begin(), data.end());
std::cout << "Posortowane HeapSort\n";
}
};
// Użycie
int main() {
// Utwórz procesor z QuickSort w czasie kompilacji
DataProcessor<QuickSortStrategy> processor_quick;
processor_quick.add_data(30);
processor_quick.add_data(10);
processor_quick.add_data(20);
processor_quick.process(); // Używa QuickSort
processor_quick.print_data();
// Utwórz procesor z MergeSort (inny typ!)
DataProcessor<MergeSortStrategy> processor_merge;
processor_merge.add_data(30);
processor_merge.add_data(10);
processor_merge.add_data(20);
processor_merge.process(); // Używa MergeSort
processor_merge.print_data();
return 0;
}
Kluczowe cechy
Wybór w czasie kompilacji
Algorytm jest znany już przy instancjonowaniu szablonu klasy:
DataProcessor<QuickSortStrategy> proc1; // Typ 1
DataProcessor<MergeSortStrategy> proc2; // Typ 2
// proc1 i proc2 to zupełnie różne typy!
Brak potrzeby wirtualnych funkcji
Kompilator zna typ, więc może wstawić wywołania strategii bezpośrednio:
// Kompilator dokładnie wie, która sort() jest wywoływana
// Brak wirtualnego wyszukiwania funkcji, brak pośredniej wskaźnika
void process() {
sorter.sort(data); // Bezpośrednie wywołanie, prawdopodobnie wstawione
}
Abstrakcja bez kosztów
Dla każdej instancjacji szablonu kompilator generuje kod dopasowany do typu. Przy włączonej optymalizacji (-O2) narzut spada praktycznie do zera:
// Z optymalizacjami, to kompiluje się do bezpośrednich wywołań algorytmu
// Brak dodatkowej pośredniej lub narzutu pamięci
Bezpieczeństwo typów
Różne instancje algorytmu to różne typy:
DataProcessor<QuickSortStrategy> quick_proc;
DataProcessor<MergeSortStrategy> merge_proc;
// To się nie skompiluje! Typy są różne.
// quick_proc = merge_proc; // BŁĄD
// Ale możesz je przekazać do funkcji szablonowych:
template<typename T>
void do_work(DataProcessor<T>& processor) {
processor.process();
}
do_work(quick_proc); // OK
do_work(merge_proc); // OK
Interfejs strategii (koncepcja)
W Strategy opartym na szablonach nie ma jawnej klasy bazowej. Strategia musi tylko spełniać koncepcję:
// Koncepcja: SortingStrategy musi mieć:
// - void sort(std::vector<int>&) const
// Te spełniają koncepcję:
struct QuickSortStrategy {
void sort(std::vector<int>& data) const { /* ... */ }
};
struct MergeSortStrategy {
void sort(std::vector<int>& data) const { /* ... */ }
};
// To NIE spełnia koncepcji (zła sygnatura metody):
struct BadStrategy {
void sort_data(std::vector<int>& data) const { /* ... */ }
};
// Błąd: DataProcessor<BadStrategy> się nie skompiluje
Przykład z życia: kompresja danych
struct GzipStrategy {
std::vector<uint8_t> compress(const std::string& data) const {
// Kompresja Gzip
return /* skompresowane dane */;
}
};
struct ZstdStrategy {
std::vector<uint8_t> compress(const std::string& data) const {
// Kompresja Zstandard
return /* skompresowane dane */;
}
};
struct NoCompressionStrategy {
std::vector<uint8_t> compress(const std::string& data) const {
// Bez kompresji, tylko skopiuj
return std::vector<uint8_t>(data.begin(), data.end());
}
};
template<typename CompressionStrategy>
class FileCompressor {
private:
CompressionStrategy strategy;
public:
std::vector<uint8_t> compress_file(const std::string& filename) {
std::string content = read_file(filename);
return strategy.compress(content);
}
};
// Użycie w czasie kompilacji
int main() {
FileCompressor<GzipStrategy> gzip_compressor;
auto compressed = gzip_compressor.compress_file("large_file.txt");
FileCompressor<NoCompressionStrategy> fast_compressor;
auto fast_copy = fast_compressor.compress_file("fast_file.txt");
return 0;
}
Zalety
- Zero narzutu w czasie wykonania: brak wywołań wirtualnych, kompilator może wstawiać kod bezpośrednio.
- Bezpieczeństwo typów: kompilator wyłapuje niezgodności strategii już na etapie kompilacji.
- Wydajność: kompilator może agresywnie optymalizować kod.
- Przyjazność dla pamięci podręcznej: monomorficzny kod (jeden typ) daje lepszą lokalność cache.
- Mały ślad binarny: każda instancjacja zawiera tylko kod swojego typu.
- Przewidywalne zachowanie: decyzje zapadają wcześnie, w czasie wykonania nie ma niespodzianek.
Wady
- Strategia musi być znana w czasie kompilacji: nie zmienisz algorytmu po kompilacji.
- Rozprzestrzenianie kodu: każda instancjacja szablonu generuje nowy kod.
- Rozmiar binarny: wiele instancjacji może zwiększyć rozmiar pliku wykonywalnego.
- Czas kompilacji: instancjonowanie szablonów wydłuża kompilację.
- Brak elastyczności w czasie wykonania: nie wczytasz strategii z pliku konfiguracyjnego ani z wtyczki.
- Trudniejsze kontenery: różnych typów nie da się łatwo trzymać w jednym kontenerze.
Porównanie: różne kontenery
// Te typy są RÓŻNE, nie można ich mieszać:
std::vector<DataProcessor<QuickSortStrategy>> processors_quick;
std::vector<DataProcessor<MergeSortStrategy>> processors_merge;
// Nie można:
std::vector<DataProcessor<?>> mixed; // Jaki typ to "?"?
// Jeśli potrzebujesz jednorodnego kontenera, użyj std::variant (C++17):
using ProcessorVariant = std::variant<
DataProcessor<QuickSortStrategy>,
DataProcessor<MergeSortStrategy>,
DataProcessor<HeapSortStrategy>
>;
std::vector<ProcessorVariant> mixed_processors;
mixed_processors.push_back(DataProcessor<QuickSortStrategy>{});
mixed_processors.push_back(DataProcessor<MergeSortStrategy>{});
// Wywołaj process na każdym (wymaga std::visit):
for (auto& proc_variant : mixed_processors) {
std::visit([](auto& proc) { proc.process(); }, proc_variant);
}
Kiedy używać wariantu opartego na szablonach
- Algorytm jest znany w czasie kompilacji.
- Zależy Ci na abstrakcji bez narzutu.
- Każda instancja algorytmu ma odrębny typ w całym programie.
- Nie musisz trzymać strategii w kontenerach (albo wystarczy Ci std::variant).
- Czas kompilacji z narzutem instancjacji szablonów jest do przyjęcia.
Jeśli algorytm trzeba wybrać w czasie wykonania, zobacz Wybór w czasie wykonania (funkcje wirtualne).
Patrz też
- Wybór w czasie wykonania (funkcje wirtualne): wybór w czasie wykonania przez funkcje wirtualne.
- Policy Based Design: zaawansowana technika szablonowa z politykami.
- Template Method: podobna nazwa, inny wzorzec (definiuje szkielet algorytmu).
Podsumowanie
Ten wariant Strategy wybiera algorytm w czasie kompilacji. Typ strategii jest parametrem szablonu, więc kompilator generuje kod dopasowany do typu i nie ma wywołań wirtualnych. W zamian tracisz elastyczność w czasie wykonania, a zyskujesz wydajność i bezpieczeństwo typów. Używaj go, gdy algorytm jest znany przy kompilacji. Jeśli algorytm pochodzi z pliku konfiguracyjnego, skończysz z wariantem i std::visit, a prosty zysk zniknie.