c++ c++11 move-semantics perfect-forwarding

c++ - Constructor usando std:: forward



c++11 move-semantics (1)

Que yo sepa, las dos formas comunes de implementar de manera eficiente un constructor en C ++ 11 son usar dos de ellas

Foo(const Bar& bar) : bar_{bar} {}; Foo(Bar&& bar) : bar_{std::move(bar)} {};

o solo uno a la moda de

Foo(Bar bar) : bar_{std::move(bar)} {};

con la primera opción que resulta en un rendimiento óptimo (por ejemplo, con suerte, una sola copia en caso de un valor de l y un solo movimiento en el caso de un valor de r), pero que necesita 2 N de sobrecarga para N variables, mientras que la segunda opción solo necesita una función al costo de un movimiento adicional al pasar en un lvalue.

Esto no debería causar demasiado impacto en la mayoría de los casos, pero seguramente ninguna opción es óptima. Sin embargo, también se podría hacer lo siguiente:

template<typename T> Foo(T&& bar) : bar_{std::forward<T>(bar)} {};

Esto tiene la desventaja de permitir variables de tipos posiblemente no deseados como el parámetro de la bar (que es un problema que estoy seguro de que se resuelve fácilmente mediante la especialización de plantillas), pero en cualquier caso el rendimiento es óptimo y el código crece de manera lineal con la cantidad de variables.

¿Por qué nadie usa algo así como adelante para este propósito? ¿No es la forma más óptima?


La gente hace constructores avanzados perfectos.

Hay costos.

Primero, el costo es que deben estar en el archivo de encabezado. En segundo lugar, cada uso tiende a dar lugar a la creación de un constructor diferente. Tercero, no puede usar la sintaxis de inicialización similar a {} para los objetos que está construyendo.

Cuarto, interactúa pobremente con los Foo(Foo const&) y Foo(Foo&&) . No los reemplazará (debido a las reglas de idioma), pero se seleccionará sobre ellos para Foo(Foo&) . Esto se puede arreglar con un poco de repetitivo SFINAE:

template<class T, std::enable_if_t<!std::is_same<std::decay_t<T>, Foo>{},int> =0 > Foo(T&& bar) : bar_{std::forward<T>(bar)} {};

que ahora ya no se prefiere a Foo(Foo const&) para argumentos de tipo Foo& . Mientras estamos en ello podemos hacer:

Bar bar_; template<class T, std::enable_if_t<!std::is_same<std::decay_t<T>, Foo>{},int> =0, std::enable_if_t<std::is_constructible<Bar, T>{},int> =0 > Foo(T&& bar) : bar_{std::forward<T>(bar)} {};

y ahora este constructor solo funciona si el argumento se puede usar para construir una bar .

Lo siguiente que querrá hacer es apoyar la construcción de la bar el estilo {} , o la construcción por partes, o la construcción de varargs donde se reenvía a la barra.

Aquí hay una variante de varargs:

Bar bar_; template<class T0, class...Ts, std::enable_if_t<sizeof...(Ts)||!std::is_same<std::decay_t<T0>, Foo>{},int> =0, std::enable_if_t<std::is_constructible<Bar, T0, Ts...>{},int> =0 > Foo(T0&&t0, Ts&&...ts) : bar_{std::forward<T0>(t0), std::forward<Ts>(ts)...} {}; Foo()=default;

Por otro lado, si añadimos:

Foo(Bar&& bin):bar_(std::move(bin));

ahora Foo( {construct_bar_here} ) sintaxis de Foo( {construct_bar_here} ) , lo cual es bueno. Sin embargo, esto no es necesario si ya tenemos el varardic anterior (o una construcción por partes similar). Aún así, a veces es bueno reenviar una lista de inicializadores, especialmente si no sabemos el tipo de bar_ cuando escribimos el código (genéricos, por ejemplo):

template<class T0, class...Ts, std::enable_if_t<std::is_constructible<Bar, std::initializer_list<T0>, Ts...>{},int> =0 > Foo(std::initializer_list<T0> t0, Ts&&...ts) : bar_{t0, std::forward<Ts>(ts)...} {};

así que si Bar es un std::vector<int> podemos hacer Foo( {1,2,3} ) y terminar con {1,2,3} dentro de bar_ .

En este punto, debes preguntarte "por qué no escribí Foo(Bar) ". ¿Es realmente tan caro mover un Bar ?

En el código genérico de la biblioteca, querrá ir tan lejos como el anterior. Pero muy a menudo tus objetos son conocidos y baratos para mover. Así que escribe el Foo(Bar) realmente simple, más bien correcto, y hazlo con todas las tonterías.

Existe un caso en el que tiene N variables que no son baratas para mover y desea eficiencia, y no desea colocar la implementación en el archivo de encabezado.

Luego, simplemente escribe un creador de Bar borrado de tipo que toma cualquier cosa que se pueda usar para crear un Bar directamente, oa través de std::make_from_tuple , y almacena la creación para una fecha posterior. A continuación, utiliza RVO para construir directamente la Bar en el lugar dentro de la ubicación de destino.

template<class T> struct make { using maker_t = T(*)(void*); template<class Tuple> static maker_t make_tuple_maker() { return [](void* vtup)->T{ return make_from_tuple<T>( std::forward<Tuple>(*static_cast<std::remove_reference_t<Tuple>*>(vtup)) ); }; } template<class U> static maker_t make_element_maker() { return [](void* velem)->T{ return T( std::forward<U>(*static_cast<std::remove_reference_t<U>*>(velem)) ); }; } void* ptr = nullptr; maker_t maker = nullptr; template<class U, std::enable_if_t< std::is_constructible<T, U>{}, int> =0, std::enable_if_t<!std::is_same<std::decay_t<U>, make>{}, int> =0 > make( U&& u ): ptr( (void*)std::addressof(u) ), maker( make_element_maker<U>() ) {} template<class Tuple, std::enable_if_t< !std::is_constructible<T, Tuple>{}, int> =0, std::enable_if_t< !std::is_same<std::decay_t<Tuple>, make>{}, int> =0, std::enable_if_t<(0<=std::tuple_size<std::remove_reference_t<Tuple>>{}), int> = 0 // SFINAE test that Tuple is a tuple-like // TODO: SFINAE test that using Tuple to construct T works > make( Tuple&& tup ): ptr( std::addressof(tup) ), maker( make_tuple_maker<Tuple>() ) {} T operator()() const { return maker(ptr); } };

El código utiliza una función de C ++ 17, std::make_from_tuple , que es relativamente fácil de escribir en C ++ 11. En C ++ 17, elision garantizada significa que incluso funciona con tipos no móviles, lo que es realmente genial.

Ejemplo vivo .

Ahora puedes escribir:

Foo( make<Bar> bar_in ):bar_( bar_in() ) {}

y el cuerpo de Foo::Foo se puede mover fuera del archivo de encabezado.

Pero eso es más demente que las alternativas anteriores.

Una vez más, ¿has considerado simplemente escribir Foo(Bar) ?