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ż

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.