c++ - retornan - parametros por valor y referencia c
Evite el crecimiento exponencial de referencias de referencias y referencias de valores en el constructor (4)
¿A qué distancia del hoyo de conejo quieres ir?
Soy consciente de 4 formas decentes para abordar este problema. Por lo general, debe usar los anteriores si coincide con sus condiciones previas, ya que cada uno posterior aumenta significativamente en complejidad.
En su mayor parte, cualquier movimiento es tan barato hacerlo dos veces es gratis, o mover es copia.
Si move es copy, y copy no es free, toma el parámetro por const&
. Si no, tómalo por valor.
Esto se comportará básicamente de manera óptima y hará que su código sea mucho más fácil de entender.
LinearClassifier(Loss loss, Optimizer const& optimizer)
: _loss(std::move(loss))
, _optimizer(optimizer)
{}
para un optimizer
Loss
y movimiento de copia barata.
Esto hace 1 movimiento extra sobre el reenvío perfecto "óptimo" a continuación (nota: el reenvío perfecto no es óptimo) por parámetro de valor en todos los casos. Siempre que moverse sea barato, esta es la mejor solución, ya que genera mensajes de error limpios, permite una construcción basada en {}
, y es mucho más fácil de leer que cualquiera de las otras soluciones.
Considera usar esta solución.
Si la mudanza es más barata que la copia pero no es gratuita, un enfoque es el envío perfecto basado en: O bien:
template<class L, class O >
LinearClassifier(L&& loss, O&& optimizer)
: _loss(std::forward<L>(loss))
, _optimizer(std::forward<O>(optimizer))
{}
O los más complejos y más amigables con la sobrecarga:
template<class L, class O,
std::enable_if_t<
std::is_same<std::decay_t<L>, Loss>{}
&& std::is_same<std::decay_t<O>, Optimizer>{}
, int> * = nullptr
>
LinearClassifier(L&& loss, O&& optimizer)
: _loss(std::forward<L>(loss))
, _optimizer(std::forward<O>(optimizer))
{}
esto le cuesta la capacidad de hacer {}
construcción basada en sus argumentos. Además, el código anterior puede generar hasta un número exponencial de constructores si se llaman (ojalá estén en línea).
Puede descartar la cláusula std::enable_if_t
a costa de la falla de SFINAE; básicamente, la sobrecarga errónea de tu constructor se puede elegir si no tienes cuidado con esa cláusula std::enable_if_t
. Si tiene sobrecargas de constructor con el mismo número de argumentos, o si le preocupa el fracaso anticipado, entonces quiere el std::enable_if_t
uno. De lo contrario, use el más simple.
Esta solución generalmente se considera "más óptima". Es acertadamente óptimo, pero no es lo más óptimo.
El siguiente paso es usar construcción emplace con tuplas.
private:
template<std::size_t...LIs, std::size_t...OIs, class...Ls, class...Os>
LinearClassifier(std::piecewise_construct_t,
std::index_sequence<LIs...>, std::tuple<Ls...>&& ls,
std::index_sequence<OIs...>, std::tuple<Os...>&& os
)
: _loss(std::get<LIs>(std::move(ls))...)
, _optimizer(std::get<OIs>(std::move(os))...)
{}
public:
template<class...Ls, class...Os>
LinearClassifier(std::piecewise_construct_t,
std::tuple<Ls...> ls,
std::tuple<Os...> os
):
LinearClassifier(std::piecewise_construct_t{},
std::index_sequence_for<Ls...>{}, std::move(ls),
std::index_sequence_for<Os...>{}, std::move(os)
)
{}
donde LinearClassifier
la construcción hasta dentro del LinearClassifier
. Esto le permite tener objetos no copiables / movibles en su objeto, y podría decirse que es máximamente eficiente.
Para ver cómo funciona esto, por ejemplo ahora piecewise_construct
funciona con std::pair
. forward_as_tuple
primero por construcción por partes, luego por forward_as_tuple
los argumentos para construir luego cada elemento (incluyendo una copia o mover el cursor).
Al construir objetos directamente, podemos eliminar un movimiento o una copia por objeto en comparación con la solución de reenvío perfecto anterior. También le permite reenviar una copia o un movimiento si es necesario.
Una última técnica linda es la construcción de borrado de tipos. Prácticamente, esto requiere algo como std::experimental::optional<T>
para estar disponible, y puede hacer que la clase sea un poco más grande.
Esto no es más rápido que el de construcción por partes. Resume el trabajo que hace la construcción de emplazamientos, simplificando el uso por usuario, y le permite dividir el cuerpo del ctor del archivo de encabezado. Pero hay una pequeña cantidad de sobrecarga, tanto en tiempo de ejecución como en el espacio.
Hay un montón de repetitivo con el que necesitas comenzar. Esto genera una clase de plantilla que representa el concepto de "construir un objeto, más tarde, en un lugar que alguien más me dirá".
struct delayed_emplace_t {};
template<class T>
struct delayed_construct {
std::function< void(std::experimental::optional<T>&) > ctor;
delayed_construct(delayed_construct const&)=delete; // class is single-use
delayed_construct(delayed_construct &&)=default;
delayed_construct():
ctor([](auto&op){op.emplace();})
{}
template<class T, class...Ts,
std::enable_if_t<
sizeof...(Ts)!=0
|| !std::is_same<std::decay_t<T>, delayed_construct>{}
,int>* = nullptr
>
delayed_construct(T&&t, Ts&&...ts):
delayed_construct( delayed_emplace_t{}, std::forward<T>(t), std::forward<Ts>(ts)... )
{}
template<class T, class...Ts>
delayed_construct(delayed_emplace_t, T&&t, Ts&&...ts):
ctor([tup = std::forward_as_tuple(std::forward<T>(t), std::forward<Ts>(ts)...)]( auto& op ) mutable {
ctor_helper(op, std::make_index_sequence<sizeof...(Ts)+1>{}, std::move(tup));
})
template<std::size_t...Is, class...Ts>
static void ctor_helper(std::experimental::optional<T>& op, std::index_sequence<Is...>, std::tuple<Ts...>&& tup) {
op.emplace( std::get<Is>(std::move(tup))... );
}
void operator()(std::experimental::optional<T>& target) {
ctor(target);
ctor = {};
}
explicit operator bool() const { return !!ctor; }
};
donde escribimos-borramos la acción de construir un argumento opcional a partir de arbitrarios.
LinearClassifier( delayed_construct<Loss> loss, delayed_construct<Optimizer> optimizer ) {
loss(_loss);
optimizer(_optimizer);
}
donde _loss
son _loss
std::experimental::optional<Loss>
. Para eliminar la opción de _loss
, debe usar std::aligned_storage_t<sizeof(Loss), alignof(Loss)>
y tenga mucho cuidado al escribir un ctor para manejar excepciones y destruir cosas manualmente, etc. Es un dolor de cabeza.
Algunas cosas buenas acerca de este último patrón es que el cuerpo del cursor puede salir del encabezado, y como máximo se genera una cantidad lineal de código en lugar de una cantidad exponencial de constructores de plantilla.
Esta solución es marginalmente menos eficiente que la versión de la construcción de la ubicación, ya que no todos los compiladores podrán alinear el uso de la std::function
estándar. Pero también permite almacenar objetos no movibles.
Código no probado, por lo que probablemente haya errores tipográficos.
Estoy codificando algunas clases de plantillas para una biblioteca de aprendizaje automático, y estoy enfrentando este problema muchas veces. Estoy usando principalmente el patrón de política, donde las clases reciben como políticas de argumentos de plantilla para diferentes funcionalidades, por ejemplo:
template <class Loss, class Optimizer> class LinearClassifier { ... }
El problema es con los constructores. A medida que crece la cantidad de políticas (parámetros de plantilla), las combinaciones de referencias de referencias y referencias de valores aumentan exponencialmente. En el ejemplo anterior:
LinearClassifier(const Loss& loss, const Optimizer& optimizer) : _loss(loss), _optimizer(optimizer) {}
LinearClassifier(Loss&& loss, const Optimizer& optimizer) : _loss(std::move(loss)), _optimizer(optimizer) {}
LinearClassifier(const Loss& loss, Optimizer&& optimizer) : _loss(loss), _optimizer(std::move(optimizer)) {}
LinearClassifier(Loss&& loss, Optimizer&& optimizer) : _loss(std::move(loss)), _optimizer(std::move(optimizer)) {}
¿Hay alguna forma de evitar esto?
En aras de la exhaustividad, el constructor óptimo de dos argumentos tomaría dos referencias de reenvío y usaría SFINAE para asegurarse de que son los tipos correctos. Podemos presentar el siguiente alias:
template <class T, class U>
using decays_to = std::is_convertible<std::decay_t<T>*, U*>;
Y entonces:
template <class L, class O,
class = std::enable_if_t<decays_to<L, Loss>::value &&
decays_to<O, Optimizer>::value>>
LinearClassifier(L&& loss, O&& optimizer)
: _loss(std::forward<L>(loss))
, _optimizer(std::forward<O>(optimizer))
{ }
Esto garantiza que solo aceptemos argumentos del tipo Loss
y Optimizer
(o que se deriven de ellos). Desafortunadamente, es bastante agotador escribir y distrae mucho de la intención original. Es bastante difícil hacerlo bien, pero si el rendimiento importa, entonces sí importa, y esta es realmente la única manera de hacerlo.
Pero si no tiene importancia, y si Loss
y Optimizer
son baratos de mover (o, mejor aún, el rendimiento de este constructor es completamente irrelevante), prefiera la solución de Ilya Popov :
LinearClassifier(Loss loss, Optimizer optimizer)
: _loss(std::move(loss))
, _optimizer(std::move(optimizer))
{ }
En realidad, esta es la razón precisa por la que se introdujo el reenvío perfecto . Reescribe el constructor como
template <typename L, typename O>
LinearClassifier(L && loss, O && optimizer)
: _loss(std::forward<L>(loss))
, _optimizer(std::forward<O>(optimizer))
{}
Pero probablemente sea mucho más simple hacer lo que sugiere Ilya Popov en su answer . Para ser honesto, generalmente lo hago de esta manera, ya que los movimientos tienen la intención de ser baratos y un movimiento más no cambia las cosas de manera espectacular.
Como Howard Hinnant dijo , mi método puede ser poco amigable con SFINAE, ya que LinearClassifier acepta cualquier par de tipos en el constructor. La respuesta de Barry muestra cómo lidiar con eso.
Este es exactamente el caso de uso para la técnica "pasar por valor y mover". Aunque es un poco menos eficiente que las sobrecargas lvalue / rvalue, no está mal (un movimiento extra) y le ahorra la molestia.
LinearClassifier(Loss loss, Optimizer optimizer)
: _loss(std::move(loss)), _optimizer(std::move(optimizer)) {}
En el caso del argumento lvalue, habrá una copia y un movimiento, en el caso del argumento rvalue, habrá dos movimientos (siempre que las clases de Loss
y Optimizer
implementen constructores de movimiento).
Actualización: en general, la solución de reenvío perfecta es más eficiente. Por otro lado, esta solución evita constructores con plantilla que no siempre son deseables, ya que aceptará argumentos de cualquier tipo cuando no estén limitados con SFINAE y conducir a errores difíciles dentro del constructor si los argumentos no son compatibles. En otras palabras, los constructores con plantillas sin restricciones no son compatibles con SFINAE. Vea la respuesta de Barry para un constructor de plantilla restringido que evita este problema.
Otro posible problema de un constructor con plantilla es la necesidad de colocarlo en un archivo de cabecera.
Actualización 2: Herb Sutter habla sobre este problema en su charla de CppCon 2014 "Back to the Basics" a partir de la 1:03:48 . Habla primero de pasar por valor, luego de sobrecargar en rvalue-ref, luego de reenvío perfecto a la 1:15:22 incluyendo restricción. Y finalmente habla de los constructores como el único caso de buen uso para pasar de valor a 1:25:50 .