I have a question regarding partial specialisation of templated member functions.
Background: The goal is to compute descriptive statistics of large datasets which are too large to be hold in memory at once. Therefore I have accumulator classes for the variance and the covariance where I can push in the datasets piece by piece (either one value at a time or in larger chunks). A rather simplified version computing the arithmetic mean only is
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;
}
}
};
One particular advantage of this kind of accumulator class is the possibility to push values of different datatypes into the same accumulator class.
Problem: This works fine for all integral datatypes. However the accumulator classes should be able to handle complex numbers as well by first calculating the absolute value |z| and then pushing it to the accumulator. For pushing single values it's easy to provide an overloaded method
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));
}
For pushing chunks of data via iterators however the case not quite as simple. In order to overload correctly a partial specialisation is required since we need to know the actual (fully specialised) complex number type. The usual way would be to delegate the actual code in an internal struct and specialise it accordingly
// 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;
}
}
};
In the accumulator class the templated methods of the delegation struct are then called by
template <class InputIt>
void push(InputIt first, InputIt last)
{
push_impl<InputIt, typename std::iterator_traits<InputIt>::value_type>::push(first, last);
}
However there is one problem with this technique which is how to access the private members of the accumulator class. Since they are different classes no direct access is possible and furthermore the methods of push_impl need to be static and cannot access the non-static members of the accumulator.
I can think of the following four solutions to the problem which all have their own advantages and disadvantages:
- Create an instance of push_impl in each call to push with (possible) decrease in performance due to the extra copy.
- Have an instance of push_impl as member variable of the accumulator class, which would prevent me from pushing different datatypes into the accumulator since the instance would have to be fully specialised.
- Make all members of the accumulator class public and pass *this to push_impl::push() calls. This is a particular bad solution due to the break in encapsulation.
- Implement the iterator version in terms of the single value version, i.e. call the push() method for each element with (possible) decrease in performance due to the extra function call.
Note that the mentioned decreases performance are theoretical in their nature and might be no problem at all due to clever inlining by the compiler, however the actual push methods might be much more complex than the example from above.
Is one solution preferable to the others or do I miss something?
Best regards and many thanks.
As commented, you don't need to use partial specialization for this at all, indeed partial specialization is usually pretty easy to avoid, and preferred to avoid.
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> {});
}
Since push_impl
is a (private) member function you don't need to do anything special any more.
Compared to your proposed solutions, this has no extra performance cost. It's the same number of function calls, the only difference is passing a stateless type by value, which is a wholly trivial optimization for the compiler. And there's no sacrifice in encapsulation either. And slightly less boilerplate.
push_impl
can be made either an inner class template (if you use c++11) or a friend class template of your accumulator class (this seems like a good case for using friend declarations, since push_impl
is essentially an integral part of your accumulator class implementation, separated purely for language reasons). Then you can use your option #3 (passing this
to static methods of push_impl
), but without making accumulator members public.
Option #4 doesn't seem too bad either (since it avoids code duplication), but as you mentioned the performance impacts would need to be measured.
Personally I'd be likely to choose your option 4, after all the only part of the iterator version that actually varies with type is the logic in the "single value version"
However another option is to write your iterator versions to receive the mean and size by reference, mean and size can then be updated without them having to be made public.
This will also help with testing as it allows push_impl to be tested separately (although with this approach you might consider that's no longer the best name for the function)
As an aside, it would be better for your push_impl to be templated only on iterator type, you can deduce the value type inside push_impl in the same way that you currently so in your calling example, but with only the iterator type as a parameter there's no chance of accidently calling it with the wrong value type (which might not always cause a compilation error if the value type can be converted to the type you pass as "T")
来源:https://stackoverflow.com/questions/46100390/partial-member-function-template-specialisation-and-data-member-access