部分成员函数模板特化和数据成员访问

4
我有一个关于模板成员函数部分特化的问题。
背景:目标是计算大型数据集的描述性统计信息,这些数据集太大而无法一次性存储在内存中。因此,我有方差和协方差的累加器类,可以逐个推入数据集片段(可以一次推入一个值或更大的块)。一个相当简化的版本仅计算算术平均数。
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方法可能比上面的示例复杂得多。
是否有一种解决方案比其他解决方案更可取,或者我忽略了一些东西?
最好的问候并致以感谢。

函数模板的部分特化也可以通过标签派发技术来模拟实现。 - Constructor
我喜欢选项1。它很干净,封装得很好,你会发现在任何合理的编译器上,“创建”和“复制”一个无状态类作为局部变量实际上不会编译成任何东西。通常最好从干净的代码开始,稍后再进行优化。 - Nemo
3个回答

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
@Stefan 有两点需要注意。首先,它不是在编译时“可以”被推断出来,而是必须在编译时被推断出来。因此肯定不会有任何间接调用的开销,很可能会进行内联化。其次,如果您认为这个答案解决了您的问题,请随意点赞/接受它:-)。 - Nir Friedman
抱歉,表述有些拙劣,我的意思是编译器可以通过优化来消除函数调用间接性。 - Stefan

0

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

选项 #4 也不太糟糕(因为它避免了代码重复),但正如您所提到的,性能影响需要进行测量。


0

个人而言,我倾向于选择您的第四个选项,毕竟迭代器版本中唯一与类型有关的部分是“单值版本”中的逻辑。

然而,另一个选择是编写接收平均值和大小的迭代器版本,并通过引用更新平均值和大小,这样它们就不必公开。

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

顺便说一下,最好将push_impl函数模板化为仅基于迭代器类型,您可以在push_impl函数内部推断出值类型,就像您当前在调用示例中所做的那样,但是只有迭代器类型作为参数,就没有机会意外地使用错误的值类型调用它(如果值类型可以转换为您传递的“T”类型,则可能不会始终导致编译错误)。


你能举个例子来说明你最后一点吗?根据我的理解,我需要第二个模板参数T(以及std::complex<T>的特化)来确保专门版本仅适用于std::complex类型,否则InputIt :: value_type可以是任何声明自己为value_type typedef的类型,比如std::vector - Stefan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接