c++ c++11 auto expression-templates

c++ - Proporcionar diferentes implementaciones de una clase dependiendo de lvalue/rvalue cuando se usan plantillas de expresión



c++11 auto (5)

Aquí hay otro intento de resolver el problema de las referencias pendientes. Sin embargo, no resuelve el problema de las referencias a cosas que se modifican.

La idea es almacenar los temporales en valores, pero tener referencias a valores (que podemos esperar para seguir viviendo después de ; ).

// Temporary => store a copy // Otherwise, store a reference template <typename T> using URefUnlessTemporary_t = std::conditional_t<std::is_rvalue_reference<T&&>::value , std::decay_t<T> , T&&> ; template <typename LHS, typename RHS> struct StringExpression { StringExpression(StringExpression const&) = delete; StringExpression(StringExpression &&) = default; constexpr StringExpression(LHS && lhs_, RHS && rhs_) : lhs(std::forward<LHS>(lhs_)) , rhs(std::forward<RHS>(rhs_)) { } explicit operator std::string() const { auto const len = size(*this); std::string res; res.reserve(len); append(res, *this); return res; } friend constexpr std::size_t size(StringExpression const& se) { return size(se.lhs) + size(se.rhs); } friend void append(std::string & s, StringExpression const& se) { append(s, se.lhs); append(s, se.rhs); } friend std::ostream & operator<<(std::ostream & os, const StringExpression & se) { return os << se.lhs << se.rhs; } private: URefUnlessTemporary_t<LHS> lhs; URefUnlessTemporary_t<RHS> rhs; }; template <typename LHS, typename RHS> StringExpression<LHS&&,RHS&&> operator+(LHS && lhs, RHS && rhs) { return StringExpression<LHS&&,RHS&&>{std::forward<LHS>(lhs), std::forward<RHS>(rhs) }; }

No tengo ninguna duda de que esto podría ser simplificado.

int main () { constexpr static auto c = exp::concatenator{}; { std::cout << "RVREF/n"; auto r = c + f() + "toto"; std::cout << r << "/n"; std::string s (r); std::cout << s << "/n"; } { std::cout << "/n/nLVREF/n"; std::string str="lvref"; auto r = c + str + "toto"; std::cout << r << "/n"; std::string s (r); std::cout << s << "/n"; } { std::cout << "/n/nCLVREF/n"; std::string const str="clvref"; auto r = c + str + "toto"; std::cout << r << "/n"; std::string s (r); std::cout << s << "/n"; } }

NB: No proporciono size() , append() ni concatenator , no son los puntos donde están las dificultades

PD: he usado C ++ 14 solo para simplificar los rasgos de tipo.

El problema

Supongamos que implementamos una clase de string que representa, uhm, cadenas. Luego queremos agregar un operator+ que concatene dos string s, y decidimos implementar eso a través de plantillas de expresión para evitar múltiples asignaciones al hacer str1 + str2 + ... + strN .

El operador se verá así:

stringbuilder<string, string> operator+(const string &a, const string &b)

stringbuilder es una clase de plantilla, que a su vez sobrecarga al operator+ y tiene un operador de conversión de string implícito. Más o menos el ejercicio estándar de libros de texto:

template<class T, class U> class stringbuilder; template<> class stringbuilder<string, string> { stringbuilder(const string &a, const string &b) : a(a), b(b) {}; const string &a; const string &b; operator string() const; // ... } // recursive case similar, // building a stringbuilder<stringbuilder<...>, string>

La implementación anterior funciona perfectamente mientras alguien lo haga.

string result = str1 + str2 + ... + strN;

Sin embargo, tiene un error sutil . Asignar el resultado a una variable del tipo correcto hará que esa variable mantenga referencias a todas las cadenas que componen la expresión. Eso significa, por ejemplo, que cambiar una de las cadenas cambiará el resultado:

void print(string); string str1 = "foo"; string str2 = "bar"; right_type result = str1 + str2; str1 = "fie"; print(result);

Esto imprimirá fiebar , debido a la referencia str1 almacenada dentro de la plantilla de expresión. Se pone peor:

string f(); right_type result = str1 + f(); print(result); // kaboom

Ahora la plantilla de expresión contendrá una referencia a un valor destruido, lo que colapsará su programa de inmediato.

Ahora, ¿qué es ese tipo right_type ? Por supuesto, es stringbuilder<stringbuilder<...>, string> , es decir, el tipo que la plantilla de expresión magic está generando para nosotros.

Ahora, ¿por qué uno usaría un tipo oculto como ese? De hecho, uno no lo usa explícitamente, ¡ pero el auto de C ++ 11 sí!

auto result = str1 + str2 + ... + strN; // guess what''s going on here?

La pregunta

La conclusión es: parece que esta forma de implementar plantillas de expresión (almacenando referencias baratas en lugar de copiar valores o utilizando punteros compartidos) se interrumpe tan pronto como uno intenta almacenar la plantilla de expresión en sí.

Por lo tanto, me gustaría mucho una forma de detectar si estoy creando un valor de r o un valor de l , y proporcionar diferentes implementaciones de la plantilla de expresión dependiendo de si un valor de r se construye (mantener referencias) o un valor de l se construye (hacer copias ).

¿Existe un patrón de diseño estable para manejar esta situación?

Las únicas cosas que pude averiguar durante mi investigación fueron que

  1. Uno puede sobrecargar las funciones miembro dependiendo de que sea un valor lvalue o rvalue, es decir

    class C { void f() &; void f() &&; // called on temporaries }

    Sin embargo, parece que no puedo hacer eso en los constructores también.

  2. En C ++ realmente no se puede hacer `` sobrecargas de tipo '''' , es decir, ofrecer implementaciones múltiples del mismo tipo, dependiendo de cómo se vaya a usar el tipo (instancias creadas como valores de l o valores).


Comencé esto en un comentario pero era un poco grande para eso. Luego, hagamos una respuesta (aunque no responda realmente a tu pregunta).

Este es un problema conocido con auto . Por ejemplo, ha sido discutido por Herb Sutter here y con más detalles por Motti Lanzkron here .

Como dicen, hubo discusiones en el comité para agregar el operator auto a C ++ para abordar este problema. La idea sería en lugar de (o además de) proporcionar

operator string() const;

como usted mencionó, uno proporcionaría

string operator auto() const;

Para ser utilizado en contextos de deducción de tipo. En este caso,

auto result = str1 + str2 + ... + strN;

no deduciría que el tipo de result es el "tipo correcto" sino la string tipo porque eso es lo operator auto() devuelve el operator auto() .

AFAICT esto no va a suceder en C ++ 14. C ++ 17 pehaps ...


Elaborando un comentario que hice al OP ; ejemplo:

Esto solo aborda el problema de asignar un objeto o vincularlo a una referencia y luego convertirlo a un tipo de destino. No es una solución completa al problema (también vea la respuesta de Yakk a mi comentario ), pero evita el escenario presentado en el OP y hace que generalmente sea más difícil escribir este tipo de código propenso a errores.

Edición: Puede que no sea posible expandir este enfoque para las plantillas de clase (más específicamente, la especialización de std::move ). Macro''ing podría funcionar para este problema específico, pero obviamente es feo. Sobrecarga std::move dependería de UB.

#include <utility> #include <cassert> // your stringbuilder class struct wup { // only use member functions with rvalue-ref-qualifier // this way, no lvalues of this class can be used operator int() && { return 42; } }; // specialize `std::move` to "prevent" from converting lvalues to rvalue refs // (make it much harder and more explicit) namespace std { template<> wup&& move(wup&) noexcept { assert(false && "Do not use `auto` with this expression!"); } // alternatively: no function body -> linker error } int main() { auto obj = wup{}; auto& lref = obj; auto const& clref = wup{}; auto&& rref = wup{}; // fail because of conversion operator int iObj = obj; int iLref = lref; int iClref = clref; int iRref = rref; int iClref_mv = std::move(clref); // assert because of move specialization int iObj_mv = std::move(obj); int iLref_mv = std::move(lref); int iRref_mv = std::move(rref); // works int i = wup{}; }


Solo una idea descabellada (no la he probado):

template<class T, class U> class stringbuilder { stringbuilder(stringbuilder const &) = delete; }

¿No forzaría error de compilación?


Un posible enfoque sería utilizar el patrón de objeto nulo. Si bien puede hacer que el generador de cadenas sea más grande, aún evitará las asignaciones de memoria.

template <> class stringbuilder<std::string,std::string> { std::string lhs_value; std::string rhs_value; const std::string& lhs; const std::string& rhs; stringbuilder(const std::string &lhs, const std::string &rhs) : lhs(lhs), rhs(rhs) {} stringbuilder(std::string&& lhs, const std::string &rhs) : lhs_value(std::move(lhs)), lhs(lhs_value), rhs(rhs) {} stringbuilder(const std::string& lhs, std::string&& rhs) : rhs_value(std::move(rhs)), lhs(lhs), rhs(rhs_value) {} stringbuilder(std::string&& lhs, std::string&& rhs) : lhs_value(std::move(lhs)), rhs_value(std::move(rhs)), lhs(lhs_value), rhs(rhs_value) {} //...

Si el argumento al constructor es un valor l, entonces almacena una referencia al objeto real. Si el argumento al constructor es un rvalor, puede moverlo a una variable interna casi sin costo (las operaciones de movimiento son baratas) y almacenar una referencia a ese objeto interno. El resto del código puede acceder a la referencia sabiendo (bueno, al menos esperando ) que la cadena aún estará viva.

La parte esperada es porque no hay nada que bloquee el uso indebido si se pasa un valor límico, pero el objeto se destruye antes de que el constructor de cadenas complete su trabajo.