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ż

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.