【问题标题】:Partial member function template specialisation and data member access部分成员函数模板特化和数据成员访问
【发布时间】:2017-09-07 15:40:03
【问题描述】:

我有一个关于模板化成员函数的部分专业化的问题。

背景:目标是计算大型数据集的描述性统计数据,这些数据集太大而无法一次保存在内存中。因此,我有方差和协方差的累加器类,我可以在其中逐个推入数据集(一次一个值或更大的块)。仅计算算术平均值的相当简化的版本是

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 的方法需要是静态的,不能访问累加器的非静态成员。

我可以想到以下四种解决问题的方法,它们各有优缺点:

  1. 在每次调用 push 时创建一个 push_impl 实例,但由于额外的副本(可能)会降低性能。
  2. 有一个 push_impl 的实例作为累加器类的成员变量,这将阻止我将不同的数据类型推送到累加器中,因为该实例必须完全专门化。
  3. 公开累加器类的所有成员并将 *this 传递给 push_impl::push() 调用。由于封装中断,这是一个特别糟糕的解决方案。
  4. 根据单值版本实现迭代器版本,即为每个元素调用 push() 方法,由于额外的函数调用,性能(可能)下降。

请注意,所提到的性能下降本质上是理论上的,由于编译器的巧妙内联,可能根本没有问题,但是实际的 push 方法可能比示例中的复杂得多以上。

一种解决方案比其他解决方案更可取还是我错过了什么?

最好的问候和非常感谢。

【问题讨论】:

  • 函数模板的部分特化也可以通过标签调度技术来模拟。
  • 我喜欢选项 1。它干净、封装良好,您应该会发现,在任何合理的编译器上,“创建”和“复制”一个无状态类作为局部变量实际上不会编译为任何内容。通常最好从干净的代码开始,然后再进行优化。

标签: c++ templates template-specialization partial-specialization


【解决方案1】:

正如评论的那样,您根本不需要为此使用偏特化,确实偏特化通常很容易避免,并且最好避免。

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 是一个(私有)成员函数,您不再需要做任何特别的事情。

与您提出的解决方案相比,这没有额外的性能成本。它是相同数量的函数调用,唯一的区别是按值传递无状态类型,这对编译器来说是一个完全微不足道的优化。封装也没有牺牲。而且样板文件略少。

【讨论】:

  • 非常简洁的解决方案,比其他四个短得多,并且由于可以在编译时推断出正确的push_impl 版本,因此很可能会优化掉。
  • @Stefan 两件事。首先,它不是“可以”在编译时推导出来的,它必须在编译时推导出来。所以肯定不会有间接调用的任何开销,而且几乎可以肯定它会被内联。其次,如果您觉得这回答了您的问题,请随时投票/接受它:-)。
  • 对不起,公式有点笨拙,我的意思是函数调用间接可以被编译器优化掉。
【解决方案2】:

push_impl 可以作为内部类模板(如果您使用 c++11)或累加器类的友元类模板(这似乎是使用友元声明的一个好例子,因为push_impl 本质上是累加器类实现的一个组成部分,纯粹出于语言原因而分开)。然后你可以使用你的选项#3(将this 传递给push_impl 的静态方法),但不公开累加器成员。

选项 #4 似乎也不错(因为它避免了代码重复),但正如您提到的,需要衡量性能影响。

【讨论】:

    【解决方案3】:

    就我个人而言,我可能会选择您的选项 4,毕竟迭代器版本中真正随类型变化的唯一部分是“单值版本”中的逻辑

    但是另一种选择是编写迭代器版本以通过引用接收均值和大小,然后可以更新均值和大小而不必公开它们。

    这也有助于测试,因为它允许单独测试 push_impl(尽管使用这种方法,您可能认为这不再是函数的最佳名称)

    顺便说一句,最好只在迭代器类型上对 push_impl 进行模板化,您可以以与当前在调用示例中相同的方式推断 push_impl 中的值类型,但只有迭代器类型为一个参数没有机会意外地使用错误的值类型调用它(如果值类型可以转换为您作为“T”传递的类型,这可能不会总是导致编译错误)

    【讨论】:

    • 你能举一个关于你最后一点的例子吗?据我了解,我需要第二个模板参数T(以及std::complex&lt;T&gt; 专业化)以确保仅针对std::complex 类型推断专用版本,否则InputIt::value_type 可以是任何类型,自称为@987654325 @typedef 喜欢 std::vector.
    猜你喜欢
    • 2012-04-11
    • 1970-01-01
    • 2012-12-15
    • 2013-02-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多