W tym wariancie funkcje wirtualne wybierają algorytm w czasie wykonania. O wyborze decyduje konfiguracja, dane od użytkownika albo inne warunki panujące w trakcie działania programu. Płacisz za to wydajnością w porównaniu z szablonami, ale zyskujesz elastyczność.
Podejście
Tutaj algorytmy dziedziczą ze wspólnej klasy bazowej z metodami wirtualnymi:
#include <vector>
#include <memory>
#include <iostream>
#include <algorithm>
// Abstrakcyjna klasa bazowa Strategy
class SortStrategy {
public:
virtual ~SortStrategy() = default;
virtual void sort(std::vector<int>& data) = 0;
virtual std::string name() const = 0;
};
// Konkretne implementacje strategii
class QuickSortStrategy : public SortStrategy {
public:
void sort(std::vector<int>& data) override {
std::sort(data.begin(), data.end(),
[](int a, int b) { return a < b; });
std::cout << "Posortowane QuickSort\n";
}
std::string name() const override {
return "QuickSort";
}
};
class MergeSortStrategy : public SortStrategy {
public:
void sort(std::vector<int>& data) override {
std::stable_sort(data.begin(), data.end(),
[](int a, int b) { return a < b; });
std::cout << "Posortowane MergeSort\n";
}
std::string name() const override {
return "MergeSort";
}
};
class HeapSortStrategy : public SortStrategy {
public:
void sort(std::vector<int>& data) override {
std::make_heap(data.begin(), data.end());
std::sort_heap(data.begin(), data.end());
std::cout << "Posortowane HeapSort\n";
}
std::string name() const override {
return "HeapSort";
}
};
// Klasa kontekstu używająca strategii w czasie wykonania
class DataProcessor {
private:
std::vector<int> data;
std::unique_ptr<SortStrategy> strategy;
public:
DataProcessor(std::unique_ptr<SortStrategy> strategy)
: strategy(std::move(strategy)) {}
// Zmień strategię w czasie wykonania!
void set_strategy(std::unique_ptr<SortStrategy> new_strategy) {
strategy = std::move(new_strategy);
}
void add_data(int value) {
data.push_back(value);
}
void process() {
if (!strategy) {
std::cerr << "Nie ustawiono strategii\n";
return;
}
strategy->sort(data);
}
void print_data() const {
for (int value : data) {
std::cout << value << " ";
}
std::cout << "\n";
}
std::string current_strategy() const {
return strategy ? strategy->name() : "Brak";
}
};
// Funkcja fabryki do tworzenia strategii w czasie wykonania
std::unique_ptr<SortStrategy> create_strategy(const std::string& name) {
if (name == "quick") {
return std::make_unique<QuickSortStrategy>();
} else if (name == "merge") {
return std::make_unique<MergeSortStrategy>();
} else if (name == "heap") {
return std::make_unique<HeapSortStrategy>();
}
return nullptr;
}
// Użycie
int main() {
// Utwórz procesor z wybraną w czasie wykonania strategią
auto processor = std::make_unique<DataProcessor>(
std::make_unique<QuickSortStrategy>()
);
processor->add_data(30);
processor->add_data(10);
processor->add_data(20);
processor->process();
std::cout << "Używa: " << processor->current_strategy() << "\n";
processor->print_data();
// Zmień strategię w czasie wykonania
processor->set_strategy(std::make_unique<MergeSortStrategy>());
processor->add_data(50);
processor->add_data(5);
processor->process();
std::cout << "Używa: " << processor->current_strategy() << "\n";
processor->print_data();
// Utwórz strategię z konfiguracji
std::string config_algorithm = "heap";
auto from_config = std::make_unique<DataProcessor>(
create_strategy(config_algorithm)
);
from_config->add_data(100);
from_config->add_data(25);
from_config->process();
return 0;
}
Kluczowe cechy
Wybór w czasie wykonania
Algorytm wybierasz w trakcie działania programu:
// Algorytm wybrany na podstawie danych wejściowych użytkownika lub konfiguracji
std::string user_choice = get_user_input();
auto processor = std::make_unique<DataProcessor>(
create_strategy(user_choice)
);
Wywołania wirtualne
Wywołania strategii przechodzą przez funkcje wirtualne:
void process() {
// Wirtualne wywołanie - która sort() jest wywoływana zależy od typu czasu wykonania
strategy->sort(data); // Wyszukiwanie w vtable, wywołanie przez wskaźnik
}
Przechowywanie strategii w jednym kontenerze
Różne strategie mogą leżeć w tym samym kontenerze:
std::vector<std::unique_ptr<SortStrategy>> strategies;
strategies.push_back(std::make_unique<QuickSortStrategy>());
strategies.push_back(std::make_unique<MergeSortStrategy>());
strategies.push_back(std::make_unique<HeapSortStrategy>());
// Wywołaj wszystkie strategie
for (auto& strategy : strategies) {
strategy->sort(data); // Dynamiczne wysyłanie do odpowiedniej implementacji
}
Dynamiczne przełączanie strategii
Strategię możesz zmienić w dowolnym momencie:
processor->set_strategy(std::make_unique<MergeSortStrategy>());
processor->process(); // Teraz używa merge sort
processor->set_strategy(std::make_unique<HeapSortStrategy>());
processor->process(); // Teraz używa heap sort
Przykład z życia: usługa kompresji
// Klasa bazowa strategii
class CompressionStrategy {
public:
virtual ~CompressionStrategy() = default;
virtual std::vector<uint8_t> compress(const std::string& data) = 0;
virtual float compression_ratio() const = 0;
};
class GzipStrategy : public CompressionStrategy {
public:
std::vector<uint8_t> compress(const std::string& data) override {
// Implementacja kompresji Gzip
return /* skompresowane dane */;
}
float compression_ratio() const override { return 0.65f; }
};
class ZstdStrategy : public CompressionStrategy {
public:
std::vector<uint8_t> compress(const std::string& data) override {
// Implementacja kompresji Zstandard
return /* skompresowane dane */;
}
float compression_ratio() const override { return 0.62f; }
};
class NoCompressionStrategy : public CompressionStrategy {
public:
std::vector<uint8_t> compress(const std::string& data) override {
return std::vector<uint8_t>(data.begin(), data.end());
}
float compression_ratio() const override { return 1.0f; }
};
// Kontekst
class FileCompressor {
private:
std::unique_ptr<CompressionStrategy> strategy;
public:
FileCompressor(std::unique_ptr<CompressionStrategy> strat)
: strategy(std::move(strat)) {}
void set_compression_level(int level) {
// Wybierz strategię na podstawie poziomu
if (level == 0) {
strategy = std::make_unique<NoCompressionStrategy>();
} else if (level < 8) {
strategy = std::make_unique<ZstdStrategy>();
} else {
strategy = std::make_unique<GzipStrategy>();
}
}
std::vector<uint8_t> compress_file(const std::string& filename) {
auto content = read_file(filename);
return strategy->compress(content);
}
float get_ratio() const {
return strategy->compression_ratio();
}
};
// Użycie
int main() {
auto compressor = std::make_unique<FileCompressor>(
std::make_unique<ZstdStrategy>()
);
// Zmień kompresję na podstawie wyboru użytkownika lub konfiguracji
int user_level = get_compression_level();
compressor->set_compression_level(user_level);
auto compressed = compressor->compress_file("data.txt");
std::cout << "Stosunek kompresji: " << compressor->get_ratio() << "\n";
return 0;
}
Narzut funkcji wirtualnej
Wywołanie wirtualne kosztuje więcej niż bezpośrednie:
// OPARTE NA SZABLONACH (bezpośrednie wywołanie, prawdopodobnie wstawione):
template<typename Strategy>
void process(Strategy& strategy) {
strategy.sort(data); // Bezpośrednie wywołanie, brak pośredniej
}
// Asembler: jmp <direct_address>
// WYBÓR W CZASIE WYKONANIA (wirtualne wywołanie funkcji):
void process() {
strategy->sort(data); // Wirtualne wywołanie przez vtable
}
// Asembler:
// mov rax, [rdi] ; załaduj wskaźnik vtable
// mov rax, [rax+X] ; załaduj wskaźnik funkcji z vtable
// call [rax] ; wywołaj przez wskaźnik funkcji
Typowy narzut:
- 1-3 cykle CPU na odczyt vtable i wywołanie funkcji.
- Kara za błędną predykcję rozgałęzienia, gdy wzorce wywołań się różnią.
- Mniej okazji do wstawiania kodu, bo kompilator nie wstawia funkcji wirtualnych.
- Dodatkowe obciążenie pamięci podręcznej przez pośrednie odwołania do vtable.
Przykład benchmarku:
QuickSort (100M liczb całkowitych):
- Oparte na szablonach: 1250ms (bezpośrednie wywołanie, wstawione)
- Wybór w czasie wykonania: 1380ms (narzut wirtualnego wywołania ~10%)
- Różnica: ~130ms (~10% wolniejsze)
Zalety
- Elastyczność w czasie wykonania: algorytm wybierasz na podstawie warunków panujących w trakcie działania.
- Dynamiczne przełączanie strategii: zmieniasz algorytm bez ponownej kompilacji.
- Konfiguracja: strategię wczytasz z pliku konfiguracyjnego lub zmiennej środowiskowej.
- Jednorodne kontenery: różne strategie trzymasz w jednej kolekcji.
- Prostsze generowanie kodu: nie ma rozprzestrzeniania szablonów.
- Wtyczki: strategie ładujesz z zewnętrznych bibliotek.
- Łatwiejsze testowanie: strategię podmienisz przez dziedziczenie.
Wady
- Narzut funkcji wirtualnej: wywołania pośrednie kosztują.
- Brak wstawiania: kompilator zwykle nie wstawia funkcji wirtualnych.
- Sprawdzanie typów w czasie wykonania: łatwo o wskaźnik null albo zły typ.
- Więcej obiektów: każda instancja strategii wymaga alokacji pamięci.
- Mniejsze bezpieczeństwo typów: błąd wychodzi w czasie wykonania, nie kompilacji.
- Gorsza pamięć podręczna: polimorficzne wywołania psują lokalność cache.
- Ryzyko niezdefiniowanego zachowania: wskaźnik strategii może być null albo wiszący.
Porównanie wydajności
| Aspekt | Oparte na szablonach | Wybór w czasie wykonania |
|---|---|---|
| Czas wyboru algorytmu | Czas kompilacji | Czas wykonania |
| Narzut wywołania funkcji | 0 (wstawione) | 1-3 cykle CPU |
| Wstawianie | ✓ Tak | ✗ Nie (zwykle) |
| Rozprzestrzenianie kodu | Instancjacja szablonu | Minimalne |
| Rozmiar binarny | Większy (na instancję) | Mniejszy |
| Alokacja pamięci | Stos (czas kompilacji) | Sterta (unique_ptr) |
| Bezpieczeństwo typów | Czas kompilacji | Czas wykonania |
| Elastyczność | Tylko algorytmy czasu kompilacji | Wybór w czasie wykonania |
| Jednorodność kontenera | Różne typy | Ten sam typ |
| Efektywność pamięci podręcznej | Monomorficzny (lepiej) | Polimorficzny (gorzej) |
Kiedy używać wyboru w czasie wykonania
- Algorytm trzeba wybrać w czasie wykonania.
- Wybór zależy od konfiguracji, danych od użytkownika lub środowiska.
- Chcesz przełączać strategie bez ponownej kompilacji.
- Różne instancje strategii muszą być przechowywane razem.
- Architektura opiera się na wtyczkach i strategie ładują się dynamicznie.
- Narzut wywołań wirtualnych jest do przyjęcia.
Jeśli liczy się wydajność czasu kompilacji, zobacz Strategy oparty na szablonach (kompilacja).
Patrz też
- Strategy oparty na szablonach (kompilacja): podejście szablonowe z wyborem w czasie kompilacji.
- Policy Based Design: zaawansowana technika szablonowa.
- Wzorzec State: podobna struktura, ale skupia się na zachowaniu zależnym od stanu.
Podsumowanie
Strategy wyboru w czasie wykonania korzysta z funkcji wirtualnych, żeby wybrać algorytm dynamicznie. W porównaniu z szablonami tracisz wydajność, ale zyskujesz elastyczność i prostsze generowanie kodu. Liczba 10% z benchmarku to przykład, nie reguła. Zmierz własną pętlę, zanim poświęcisz wydajność dla elastyczności. Wybierz szablony, gdy algorytm jest znany przy kompilacji, a funkcje wirtualne, gdy nie jest.