Klasy cech i wytycznych

Klasy cech

Klasy cech (traits) reprezentują dodatkowe właściwości parametru szablonu, które mogą być pomocne na etapie implementacji kodu szablonu.

Case Study - Kumulowanie elementów sekwencji

template <typename T>
T accumulate(const T* begin, const T* end)
{
    T total = T{};

    while (begin != end)
    {
        total += *begin;
        ++begin;
    }

    return total;
}

Problemy:

  • określenie typu zmiennej kumulującej
  • utworzenie wartości zerowej

Parametryzacja typu zmiennej kumulującej

template <typename T>
struct AccumulationTraits;

template <>
struct AccumulationTraits<uint8_t>
{
    typedef unsigned int AccumulatorType;
};

template<>
struct AccumulationTraits<float>
{
    typedef double AccumulatorType;
};

Szablon AccumulationTraits zwany jest szablonem cechy, gdyż przechowuje cechę typu, który jest parametrem szablonu.

Algorytm korzystający z klasy cech wygląda następująco:

template <typename T>
typename AccumulationTraits<T>::AccumulatorType
accumulate(const T* begin, const T* end)
{
    using AccT = typename AccumulationTraits<T>::AccumulatorType;

    AccT total = T{};

    while (begin != end)
    {
        total += *begin;
        ++begin;
    }

    return total;
}

Cechy wartości

Klasy cech mogą również zawierać informację o stałych charakterystycznych dla opisywanego typu. Możemy w klasie cechy zdefiniować wartość zerową dla typu.

template<typename T> struct AccumulationTraits;

template<>
struct AccumulationTraits<uint8_t>
{
    using AccumulatorType = int;
    static constexpr AccumulatorType zero = 0;
};

template<>
struct AccumulationTraits<float>
{
    using AccumulatorType = float;
    static constexpr float zero = 0.0f;
};

template<>
struct AccumulationTraits<BigInt>
{
    using AccumulationTraits = BigInt;
    inline static BigInt const zero = BigInt{0};  // OK since C++17
};

Algorytm korzystający z klasy cech uwzględniającej wartość zerową:

template <typename T>
typename AccumulationTraits<T>::AccumulatorType accumulate(const T* begin, const T* end)
{
    using AccT = typename AccumulationTraits<T>::AccumulatorType;

    AccT total = AccumulationTraits<T>::zero; // wykorzystanie cechy

    while (begin != end)
    {
        total += *begin;
        ++begin;
    }

    return total;
}

Parametryzowanie cech typów

Parametryzacja cech wymaga dodatkowych parametrów szablonu. Aby stosowanie sparametryzowanych cech typów było wygodne, należy wykorzystać domyślne wartości parametrów szablonu.

template <typename T, typename Traits = AccumulationTraits<T>>
typename Traits::AccumulatorType accumulate(const T* begin, const T* end)
{
    using AccT = typename Traits::AccumulatorType;

    AccT total = Traits::zero;

    while (begin != end)
    {
        total += *begin;
        ++begin;
    }

    return total;
}

Klasy wytycznych

Wytyczne (policies) reprezentują konfigurowalne zachowania ogólnych funkcji i typów

Klasa wytycznych – klasa udostępniająca zbiór metod implementujących określony sposób zachowania (algorytm)

template
<
   typename T,
   typename AccumulationPolicy = Sum,
   typename Traits = AccumulationTraits<T>
>
typename Traits::AccumulatorType accumulate (const T* begin, const T* end)
{
    using AccT = typename Traits::AccumulatorType;

    AccT total = Traits::zero;

    while (begin != end)
    {
        AccumulationPolicy::accumulate(total, *begin);
        ++begin;
    }

    return total;
}

Domyślna klasa wytycznej:

struct Sum
{
    template <typename T1, typename T2>
    static void accumulate(T1& total, const T2& value)
    {
        total += value;
    }
};

Zmodyfikowana klasa wytycznej:

struct Multiply
{
    template <typename T1, typename T2>
    static void accumulate(T1& total, const T2& value)
    {
        total *= value;
    }
};

int main()
{
    int data[] = {1,2,3,4,5};
    // wyświetl iloczyn wszystkich wartości
    std::cout << "the product of the integer values is " <<
        accumulate<int, MultiplyPolicy>(begin(data), end(data)) << '\n';
}

Klasy parametryzowane wytycznymi

Konstruowanie klas parametryzowanych wytycznymi polega na składaniu skomplikowanego zachowania klasy z wielu małych klas (wytycznych).

Każda wytyczna:

  • Określa jeden sposób zachowania lub implementacji
  • Ustala interfejs dotyczący jednej konkretnej czynności
  • Może być implementowana na wiele sposobów

Klasa podstawowa:

template <typename T>
class Vector
{
public:
    /* constructors */

    const T& at(size_t index) const;
    void push_back(const T& value);

    /* ... etc. ... */
};

Klasa, której zachowanie jest sparametryzowane wytycznymi:

template
<
    typename T,
    typename RangeCheckPolicy,
    typename LockingPolicy = NullMutex
>
class Vector : public RangeCheckPolicy
{
    std::vector<T> items_;
    using mutex_type = LockingPolicy;
    mutable mutex_type mtx_;

public:
    using iterator = typename std::vector<T>::iterator;
    using const_iterator = typename std::vector<T>::const_iterator;

    /* ... etc. ... */
};

Przykładowa klasa wytycznej sprawdzającej poprawność zakresu:

class ThrowingRangeChecker
{
protected:
    ~ThrowingRangeChecker() = default;

    void check_range(size_t index, size_t size) const
    {
        if (index >= size)
            throw std::out_of_range("Index out of range...");
    }
};

Inna implementacja wytycznej kontrolującej zakres indeksów:

class LoggingErrorRangeChecker
{
public:
    void set_log_file(std::ostream& log_file)
    {
        log_ = &log_file;
    }

protected:
    ~LoggingErrorRangeChecker() = default;

    void check_range(size_t index, size_t size) const
    {
        if ((index >= size) && (log_ != nullptr))
            *log_ << "Error: Index out of range. Index="
                << index << "; Size=" << size << std::endl;
    }

private:
    std::ostream* log_{};
};

Implementacja metody at() wektora z uwzględnieniem wytycznych:

template
<
    typename T,
    typename RangeCheckPolicy,
    typename LockingPolicy
>
const T& Vector<T, RangeCheckPolicy, LockingPolicy>::at(size_t index) const
{
    std::lock_guard<mutex_type> lk{mtx_};

    RangeCheckPolicy::check_range(index, size());

    return (index < items_.size()) ? items_[index] : items_.back();
}

Kod klienta:

Vector<int, ThrowingRangeChecker, StdLock> vec = {1, 2, 3};
vec.push_back(4);

try
{
    auto value = vec.at(8);
}
catch(const std::out_of_range& e)
{
    std::cout << e.what() << std::endl;
}

Klasa cech iteratorów

Biblioteka standardowa C++ często wykorzystuje technikę cech i wytycznych. Jedną z bardziej przydatnych klas cech w bibliotece standardowej, jest klasa cech iteratorów.

template <class Iterator>
struct iterator_traits
{
    typedef typename Iterator::iterator_category iterator_category;
    typedef typename Iterator::value_type value_type;
    typedef typename Iterator::difference_type difference_type;
    typedef typename Iterator::pointer pointer;
    typedef typename Iterator::reference reference;
};

template <class T>
struct iterator_traits<T*>
{
    typedef random_access_iterator_tag iterator_category;
    typedef T value_type;
    typedef ptrdiff_t difference_type;
    typedef T* pointer;
    typedef T& reference;
};

Przykład wykorzystania klasy cech iteratorów:

#include <iterator>
template <typename Iter>
inline
typename std::iterator_traits<Iter>::value_type accum (Iter start, Iter end)
{
    typedef typename std::iterator_traits<Iter>::value_type VT;
    VT total = VT(); // assume T() actually creates a zero value
    while (start != end)
    {
        total += *start;
        ++start;
    }
    return total;
}

Tag dispatching

Czasami pożądane jest dostarczenie wyspecjalizowanych implementacji dla wybranej funkcji lub klasy w celu poprawy wydajności lub uniknięcia problemów.

Przykładem może być implementacja funkcji advance_iter(), która przesuwa iterator it o zadaną n ilość kroków.

Generyczna implementacja może operować na dowolnym typie iteratora:

template<typename InputIterator, typename Distance>
void advance_iter(InputIterator& x, Distance n)
{
    while (n > 0)
    {
        ++x;
        --n;
    }
}

Nie jest to implementacja optymalna dla iteratorów o swobodnym dostępie (np. vector<int>::iterator).

Optymalizacja funkcji polega na utworzeniu grupy funkcji pomocniczych, które mogą być dopasowane do odpowiedniego rodzaju iteratora za pomocą „taga”, który umożliwia przeciążenie tych funkcji.

template<typename Iterator, typename Distance>
void advance_iter_impl(Iterator& x, Distance n, std::input_iterator_tag)
{
    // complexity - O(N)
    while (n > 0)
    {
        ++x;
        --n;
    }
}

template<typename Iterator, typename Distance>
void advance_iter_impl(Iterator& x, Distance n, std::random_access_iterator_tag)
{
    // complexity - O(1)
    x += n;
}

Funkcja advance_iter() po prostu przyjmuje argumenty i przekazuje je do funkcji pomocniczej. Na podstawie „taga” odbywa się dopasowanie odpowiedniej implementacji.

template<typename Iterator, typename Distance>
void advance_iter(Iterator& x, Distance n)
{
    advance_iter_impl(x, n,
        typename std::iterator_traits<Iterator>::iterator_category{});
}

Klasa cech std::iterator_traits umożliwia określenie rodzaju iteratora za pomocą typu iterator_category.

Biblioteka standardowa definiuje zbiór typów pełniących tagujących kategorię iteratora:

namespace std
{
    struct input_iterator_tag { };
    struct output_iterator_tag { };
    struct forward_iterator_tag : public input_iterator_tag { };
    struct bidirectional_iterator_tag : public forward_iterator_tag { };
    struct random_access_iterator_tag : public bidirectional_iterator_tag { };
}