c++ - una - Interfaz para devolver un montón de valores
funciones con devolución de valor en c++ (4)
En C ++ 11, donde la semántica de movimiento es compatible con contenedores estándar, debe ir con la opción 1 .
Hace clara la firma de su función, comunicando que solo desea que se devuelva un vector de enteros, y será eficiente, ya que no se emitirá ninguna copia: se invocará el constructor de movimientos de std::vector
(o, la mayoría probable, se aplicará la Optimización del Valor de Retorno Nombrado, resultando en ningún movimiento y sin copia):
std::vector<int> foo()
{
std::vector<int> v;
// Fill in v...
return v;
}
De esta manera, no tendrá que lidiar con problemas como la propiedad, las asignaciones dinámicas innecesarias y otras cosas que simplemente contaminan la simplicidad de su problema: devolver un montón de enteros.
En C ++ 03, puede optar por la opción 4 y tomar una referencia de valor l a un vector no const
: los contenedores estándar en C ++ 03 no son conscientes de movimientos, y copiar un vector puede ser costoso. Así:
void foo(std::vector<int>& v)
{
// Fill in v...
}
Sin embargo, incluso en ese caso, debe considerar si esta penalización es realmente importante para sus casos de uso. Si no es así, puede optar por una firma de función más clara a expensas de algunos ciclos de CPU.
Además, los compiladores de C ++ 03 son capaces de realizar la Optimización del Valor de Retorno Nombrado, por lo que aunque en teoría un temporal debe ser construido a partir del valor que devuelve, en la práctica es probable que no ocurra ninguna copia.
Tengo una función que toma un número y vuelve a esa cantidad de cosas (por ejemplo, ints). ¿Cuál es la interfaz más limpia? Algunos pensamientos:
- Devuelve un
vector<int>
. El vector sería copiado varias veces, lo cual es ineficiente. - Devuelve un
vector<int>*
. Mi captador ahora tiene que asignar el vector en sí, así como los elementos. Existen todos los problemas habituales de quién tiene que liberar el vector, el hecho de que no se puede asignar una vez y usar el mismo almacenamiento para muchas llamadas diferentes al receptor, etc. Esta es la razón por la que los algoritmos STL generalmente evitan la asignación de memoria. pasó en. - Devuelve un
unique_ptr<vector<int>>
. Ahora está claro quién lo elimina, pero todavía tenemos los otros problemas. - Tome un
vector<int>
como parámetro de referencia. El captador puedepush_back()
y la persona que llama puede decidir sireserve()
el espacio. Sin embargo, ¿qué debe hacer el captador si elvector
pasado no está vacío? ¿Adjuntar? ¿Sobrescribir borrándolo primero? Afirmar que está vacío? Sería bueno si la firma de la función permitiera una sola interpretación. - Pasar un iterador de
begin
yend
. Ahora debemos devolver el número de elementos realmente escritos (que pueden ser más pequeños de lo deseado), y la persona que llama debe tener cuidado de no acceder a los elementos que nunca fueron escritos. - Haga que el captador tome un
iterator
, y la persona que llama puede pasar uninsert_iterator
. - Renuncia y acaba de pasar un
char *
. :)
En C ++ 11, la respuesta correcta es devolver el std::vector<int>
es devolverlo, asegurando que se moverá explícita o implícitamente. (Prefiere movimiento implícito, porque el movimiento explícito puede bloquear algunas optimizaciones)
Curiosamente, si le preocupa reutilizar el búfer, la forma más sencilla es incluir un parámetro opcional que tome un std::vector<int>
por un valor como este:
std::vector<int> get_stuff( int how_many, std::vector<int> retval = std::vector<int>() ) {
// blah blah
return retval;
}
y, si tiene un búfer preasignado del tamaño correcto, simplemente std::move
a la función get_stuff
y se usará. Si no tiene un búfer preasignado del tamaño correcto, no pase un std::vector
.
Ejemplo en vivo: http://ideone.com/quqnMQ
No estoy seguro de si esto bloqueará NRVO / RVO, pero no hay una razón fundamental por la que debería hacerlo, y mover un std::vector
es lo suficientemente barato como para que a usted no le importe si de alguna manera bloquea NRVO / RVO.
Sin embargo, es posible que no desee devolver un std::vector<int>
, posiblemente solo desee iterar sobre los elementos en cuestión.
En ese caso, hay una manera fácil y una manera difícil.
La forma más fácil es exponer un for_each_element( Lambda )
:
#include <iostream>
struct Foo {
int get_element(int i) const { return i*2+1; }
template<typename Lambda>
void for_each_element( int up_to, Lambda&& f ) {
for (int i = 0; i < up_to; ++i ) {
f( get_element(i) );
}
}
};
int main() {
Foo foo;
foo.for_each_element( 7, [&](int e){
std::cout << e << "/n";
});
}
y posiblemente use una std::function
si debe ocultar la implementación de for_each
.
La forma más difícil sería devolver un generador o un par de iteradores que generen los elementos en cuestión.
Ambos evitan la asignación sin sentido del búfer cuando solo desea tratar los elementos uno a la vez, y si generar los valores en cuestión es costoso (puede ser necesario atravesar la memoria).
En C ++ 98 tomaría un vector&
y lo clear()
.
Usted mismo lo escribió:
... Esta es la razón por la cual los algoritmos STL generalmente evitan asignar memoria, en lugar de querer que se pase
excepto que los algoritmos STL no suelen "querer pasar memoria", en su lugar operan en iteradores. Esto es específicamente para desacoplar el algoritmo del contenedor, dando lugar a:
opción 8
desacoplar la generación de valor tanto del uso como del almacenamiento de esos valores, devolviendo un iterador de entrada.
La forma más fácil es usar boost::function_input_iterator
, pero hay un mecanismo de boceto a continuación (principalmente porque estaba escribiendo más rápido que pensando).
Tipo de iterador de entrada
(utiliza C ++ 11, pero puede reemplazar la std::function
con un puntero a función o simplemente codificar la lógica de la generación):
#include <functional>
#include <iterator>
template <typename T>
class Generator: public std::iterator<std::input_iterator_tag, T> {
int count_;
std::function<T()> generate_;
public:
Generator() : count_(0) {}
Generator(int count, std::function<T()> func) : count_(count)
, generate_(func) {}
Generator(Generator const &other) : count_(other.count_)
, generate_(other.generate_) {}
// move, assignment etc. etc. omitted for brevity
T operator*() { return generate_(); }
Generator<T>& operator++() {
--count_;
return *this;
}
Generator<T> operator++(int) {
Generator<T> tmp(*this);
++*this;
return tmp;
}
bool operator==(Generator<T> const &other) const {
return count_ == other.count_;
}
bool operator!=(Generator<T> const &other) const {
return !(*this == other);
}
};
Ejemplo de función de generador
(de nuevo, es trivial reemplazar el lambda con una función fuera de línea para C ++ 98, pero esto es menos tipográfico)
#include <random>
Generator<int> begin_random_integers(int n) {
static std::minstd_rand prng;
static std::uniform_int_distribution<int> pdf;
Generator<int> rv(n,
[]() { return pdf(prng); }
);
return rv;
}
Generator<int> end_random_integers() {
return Generator<int>();
}
Ejemplo de uso
#include <vector>
#include <algorithm>
#include <iostream>
int main()
{
using namespace std;
vector<int> out;
cout << "copy 5 random ints into a vector/n";
copy(begin_random_integers(5), end_random_integers(),
back_inserter(out));
copy(out.begin(), out.end(),
ostream_iterator<int>(cout, ", "));
cout << "/n" "print 2 random ints straight from generator/n";
copy(begin_random_integers(2), end_random_integers(),
ostream_iterator<int>(cout, ", "));
cout << "/n" "reuse vector storage for 3 new ints/n";
out.clear();
copy(begin_random_integers(3), end_random_integers(),
back_inserter(out));
copy(out.begin(), out.end(),
ostream_iterator<int>(cout, ", "));
}
el vector<int>
retorno vector<int>
, no se copiará, se moverá.