как typedef

я есть вопрос относительно частичной специализации шаблонных функций-членов.

Фон: Цель состоит в том, чтобы вычислить описательную статистику больших наборов данных, которые слишком велики для одновременного хранения в памяти. Поэтому у меня есть классы аккумуляторов для дисперсии и ковариации, где я могу вставлять наборы данных по частям (либо по одному значению за раз, либо в больших кусках). Довольно упрощенная версия, вычисляющая только среднее арифметическое

class Mean
{
private:
    std::size_t _size;
    double _mean;
public:
    Mean() : _size(0), _mean(0)
    {
    }
    double mean() const
    {
        return _mean;
    }
    template <class T> void push(const T value)
    {
        _mean += (value - _mean) / ++_size;
    }
    template <class InputIt> void push(InputIt first, InputIt last)
    {
        for (; first != last; ++first)
        {
            _mean += (*first - _mean) / ++_size;
        }
    }
};

Одним из особых преимуществ этого класса аккумуляторов является возможность помещать значения разных типов данных в один класс аккумуляторов.

Проблема: Это прекрасно работает для всех типов данных. Однако классы-аккумуляторы должны уметь обрабатывать и комплексные числа, сначала вычисляя абсолютное значение | z | а затем подтолкнуть его к аккумулятору. Для нажатия отдельных значений легко предоставить перегруженный метод

template <class T> void push(const std::complex<T> z)
{
    T a = std::real(z);
    T b = std::imag(z);
    push(std::sqrt(a * a + b * b));
}

Однако для передачи фрагментов данных через итераторы дело обстоит не так просто. Для корректной перегрузки требуется частичная специализация, поскольку нам нужно знать фактический (полностью специализированный) тип комплексного числа. Обычный способ состоит в том, чтобы делегировать реальный код во внутренней структуре и специализировать его соответственно

// default version for all integral types
template <class InputIt, class T>
struct push_impl
{
    static void push(InputIt first, InputIt last)
    {
        for (; first != last; ++first)
        {
            _mean += (*first - _mean) / ++_size;
        }
    }
};

// specialised version for complex numbers of any type
template <class InputIt, class T>
struct push_impl<InputIt, std::complex<T>>
{
    static void push(InputIt first, InputIt last)
    {
        for (; first != last; ++first)
        {
            T a = std::real(*first);
            T b = std::imag(*first);
            _mean += (std::sqrt(a * a + b * b) - _mean) / ++_size;
        }
    }
};

В классе аккумулятора шаблонные методы структуры делегирования затем вызываются

template <class InputIt>
void push(InputIt first, InputIt last)
{
    push_impl<InputIt, typename std::iterator_traits<InputIt>::value_type>::push(first, last);
}

Однако есть одна проблема с этой техникой, которая заключается в том, как получить доступ к закрытым членам класса аккумулятора. Поскольку это разные классы, прямой доступ невозможен, и, кроме того, методы push_impl должны быть статическими и не могут обращаться к нестатическим элементам аккумулятора.

Я могу подумать о следующих четырех решениях проблемы, у каждого из которых есть свои преимущества и недостатки:

Создать экземплярpush_impl в каждом звонкеОт себя с (возможным) снижением производительности из-за дополнительной копии.Иметь экземплярpush_impl как переменная-член класса аккумулятора, что помешало бы мне вставлять разные типы данных в аккумулятор, поскольку экземпляр должен был быть полностью специализированным.Сделайте все члены класса аккумулятора публичными и передайте*это вpush_impl::От себя() звонки. Это особенно плохое решение из-за нарушения инкапсуляции.Реализуйте версию итератора в терминах версии с одним значением, т.е. вызовитеОт себя() метод для каждого элемента с (возможным) снижением производительности из-за дополнительного вызова функции.

Обратите внимание, что упомянутое снижение производительности является теоретическим по своей природе и может не представлять никаких проблем из-за умелого встраивания компилятором, однако фактическоеОт себя методы могут быть гораздо более сложными, чем пример из выше.

Одно решение предпочтительнее других или я что-то упускаю?

С наилучшими пожеланиями и большое спасибо.

 Nemo07 сент. 2017 г., 18:37
Мне нравится вариант 1. Он чистый, хорошо инкапсулированный, и вы должны обнаружить, что «создание» и «копирование» класса без сохранения состояния как локальной переменной компилируется буквально в ничто на любом приемлемом компиляторе. Обычно лучше начинать с чистого кода и оптимизировать позже.
 Constructor07 сент. 2017 г., 18:32
Частичная специализация шаблонов функций также может быть эмулирована с помощью техники диспетчеризации тегов.

Ответы на вопрос(3)

push_impl можно сделать либо шаблон внутреннего класса (если вы используете c ++ 11), либо шаблон класса-друга вашего класса-накопителя (это кажется хорошим примером для использования объявлений друзей, посколькуpush_impl по сути является неотъемлемой частью вашей реализации класса аккумулятора, разделенной исключительно по языковым причинам). Тогда вы можете использовать свой вариант № 3 (прохождениеthis статическим методамpush_impl), но без обнародования данных об аккумуляторах.

Вариант № 4 тоже не кажется слишком плохим (поскольку он избегает дублирования кода), но, как вы упомянули, необходимо измерить влияние на производительность.

вероятно, выберу ваш вариант 4, ведь единственная часть версии итератора, которая на самом деле зависит от типа, - это логика в «версии с одним значением»

Однако другой вариант - написать свои версии итератора для получения среднего и размера по ссылке, тогда среднее и размер могут быть обновлены без их обнародования.

Это также поможет с тестированием, поскольку позволяет тестировать push_impl отдельно (хотя при таком подходе вы можете подумать, что это больше не лучшее название для функции)

Кроме того, было бы лучше, если бы ваш push_impl был шаблонизирован только для типа итератора, вы можете определить тип значения внутри push_impl так же, как вы это делаете в своем примере вызова, но только с типом итератора в качестве параметра есть нет шансов случайно вызвать его с неправильным типом значения (что может не всегда вызывать ошибку компиляции, если тип значения может быть преобразован в тип, который вы передаете как «T»)

 Stefan07 сент. 2017 г., 20:12
Не могли бы вы привести пример по последнему пункту? Из моего понимания мне нужен второй параметр шаблонаT (так же хорошо какstd::complex<T> специализация), чтобы убедиться, что специализированная версия выводится только дляstd::complex типы, в противном случаеInputIt::value_type может быть любой тип, объявляющий себяvalue_type как typedefstd::vector.
Решение Вопроса

вам вообще не нужно использовать частичную специализацию, на самом деле частичную специализацию обычно довольно легко избежать, и ее лучше избегать.

private:
template <class T>
struct tag{}; // trivial nested struct

template <class I, class T> 
void push_impl(I first, I last, tag<T>) { ... } // generic implementation

template <class I, class T>
void push_impl(I first, I last, tag<std::complex<T>>) { ... } // complex implementation

public:
template <class InputIt>
void push(InputIt first, InputIt last)
{
    push_impl(first, last,
              tag<typename std::iterator_traits<InputIt>::value_type> {});
}

посколькуpush_impl является (частной) функцией-членом, вам больше не нужно делать ничего особенного.

По сравнению с вашими предлагаемыми решениями это не требует дополнительных затрат производительности. Это такое же количество вызовов функций, единственное отличие состоит в том, что они передают тип без состояния по значению, что является совершенно тривиальной оптимизацией для компилятора. И нет никакой жертвы в инкапсуляции либо. И чуть меньше шаблонного.

 Stefan07 сент. 2017 г., 19:20
Очень аккуратное решение, которое намного короче, чем остальные четыре, и поскольку правильноеpush_impl версия может быть выведена во время компиляции, скорее всего она будет оптимизирована.
 Stefan07 сент. 2017 г., 20:15
Извините, формулировка была немного неуклюжей, я имел в виду, что компилятор может оптимизировать косвенное обращение к вызову функции.
 Nir Friedman07 сент. 2017 г., 19:48
@ Стефан Две вещи. Во-первых, дело не в том, что он «может» быть выведен во время компиляции, этодолжен быть выведено во время компиляции. Таким образом, определенно не будет никаких накладных расходов на косвенный вызов, и почти наверняка он будет встроен. Во-вторых, если вы чувствуете, что это отвечает на ваш вопрос, не стесняйтесь высказаться / принять его :-).

Ваш ответ на вопрос