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 { };
}