Aquí hay un ejemplo del mundo real en el que estoy trabajando ahora, de los sistemas de procesamiento / control de señales:
Suponga que tiene alguna estructura que representa los datos que está recopilando:
struct Sample {
time_t time;
double value1;
double value2;
double value3;
};
Ahora suponga que los mete en un vector:
std::vector<Sample> samples;
... fill the vector ...
Ahora suponga que desea calcular alguna función (por ejemplo, la media) de una de las variables en un rango de muestras, y desea factorizar este cálculo medio en una función. El puntero a miembro lo hace fácil:
double Mean(std::vector<Sample>::const_iterator begin,
std::vector<Sample>::const_iterator end,
double Sample::* var)
{
float mean = 0;
int samples = 0;
for(; begin != end; begin++) {
const Sample& s = *begin;
mean += s.*var;
samples++;
}
mean /= samples;
return mean;
}
...
double mean = Mean(samples.begin(), samples.end(), &Sample::value2);
Nota editada el 2016/08/05 para un enfoque de plantilla-función más conciso
Y, por supuesto, puede crear una plantilla para calcular una media para cualquier iterador directo y cualquier tipo de valor que admita la adición consigo mismo y la división por size_t:
template<typename Titer, typename S>
S mean(Titer begin, const Titer& end, S std::iterator_traits<Titer>::value_type::* var) {
using T = typename std::iterator_traits<Titer>::value_type;
S sum = 0;
size_t samples = 0;
for( ; begin != end ; ++begin ) {
const T& s = *begin;
sum += s.*var;
samples++;
}
return sum / samples;
}
struct Sample {
double x;
}
std::vector<Sample> samples { {1.0}, {2.0}, {3.0} };
double m = mean(samples.begin(), samples.end(), &Sample::x);
EDITAR: el código anterior tiene implicaciones de rendimiento
Debería tener en cuenta, como descubrí pronto, que el código anterior tiene algunas implicaciones de rendimiento graves. El resumen es que si está calculando una estadística de resumen en una serie de tiempo, o calculando una FFT, etc., entonces debe almacenar los valores para cada variable de forma contigua en la memoria. De lo contrario, iterar sobre la serie provocará una pérdida de caché por cada valor recuperado.
Considere el rendimiento de este código:
struct Sample {
float w, x, y, z;
};
std::vector<Sample> series = ...;
float sum = 0;
int samples = 0;
for(auto it = series.begin(); it != series.end(); it++) {
sum += *it.x;
samples++;
}
float mean = sum / samples;
En muchas arquitecturas, una instancia de Sample
llenará una línea de caché. Entonces, en cada iteración del bucle, una muestra se extraerá de la memoria al caché. Se usarán 4 bytes de la línea de caché y el resto se descartará, y la próxima iteración dará como resultado otra pérdida de caché, acceso a memoria, etc.
Mucho mejor hacer esto:
struct Samples {
std::vector<float> w, x, y, z;
};
Samples series = ...;
float sum = 0;
float samples = 0;
for(auto it = series.x.begin(); it != series.x.end(); it++) {
sum += *it;
samples++;
}
float mean = sum / samples;
Ahora, cuando el primer valor x se carga desde la memoria, los siguientes tres también se cargarán en la memoria caché (suponiendo una alineación adecuada), lo que significa que no necesita cargar ningún valor para las próximas tres iteraciones.
El algoritmo anterior se puede mejorar un poco más mediante el uso de instrucciones SIMD en, por ejemplo, arquitecturas SSE2. Sin embargo, estos funcionan mucho mejor si los valores están todos contiguos en la memoria y puede usar una sola instrucción para cargar cuatro muestras juntas (más en versiones SSE posteriores).
YMMV: diseñe sus estructuras de datos para adaptarlas a su algoritmo.