c++ - make_tuple - Haga un tipo personalizado "tie-able"(compatible con std:: tie)
tuple c++ example (4)
Por qué fallan los intentos actuales
std::tie(a, b)
produce un std::tuple<int&, string&>
. Este tipo no está relacionado con std::tuple<int, string>
etc.
std::tuple<T...>
s tiene varios operadores de asignación:
- Un operador de asignación predeterminado, que toma una
std::tuple<T...>
- Una plantilla de asignación-operador de conversión de tuplas con un paquete de parámetros de tipo
U...
, que toma unastd::tuple<U...>
- Una plantilla de asignación-operador de conversión de pares con dos parámetros de tipo
U1, U2
, que toma unstd::pair<U1, U2>
Para esas tres versiones existen variantes de copiar y mover; agregue un const&
o a &&
a los tipos que toman.
Las plantillas del operador de asignación deben deducir sus argumentos de plantilla del tipo de argumento de función (es decir, del tipo de RHS de la expresión de asignación).
Sin un operador de conversión en Foo
, ninguno de esos operadores de asignación es viable para std::tie(a,b) = foo
. Si agrega un operador de conversión a Foo
, solo se vuelve viable el operador de asignación predeterminado: la deducción del tipo de plantilla no tiene en cuenta las conversiones definidas por el usuario. Es decir, no puede deducir argumentos de plantilla para las plantillas del operador de asignación del tipo Foo
.
Como solo se permite una conversión definida por el usuario en una secuencia de conversión implícita, el tipo al que se convierte el operador de conversión debe coincidir exactamente con el tipo de operador de asignación predeterminado. Es decir, debe usar exactamente los mismos tipos de elementos de tupla que el resultado de std::tie
.
Para soportar las conversiones de los tipos de elementos (por ejemplo, la asignación de Foo::a
a a long
), el operador de conversión de Foo
tiene que ser una plantilla:
struct Foo {
int a;
string b;
template<typename T, typename U>
operator std::tuple<T, U>();
};
Sin embargo, los tipos de elementos de std::tie
son referencias. Dado que no debe devolver una referencia a un temporal, las opciones de conversión dentro de la plantilla del operador son bastante limitadas (montón, tipo de juego de palabras, estático, hilo local, etc.).
Considere que tengo un tipo personalizado (que puedo extender):
struct Foo {
int a;
string b;
};
¿Cómo puedo hacer que una instancia de este objeto se pueda asignar a std::tie
, es decir, std::tuple
de referencias?
Foo foo = ...;
int a;
string b;
std::tie(a, b) = foo;
Intentos fallidos:
La sobrecarga del operador de asignación para tuple<int&,string&> = Foo
no es posible, ya que el operador de asignación es uno de los operadores binarios que deben ser miembros del objeto del lado izquierdo.
Así que traté de resolver esto implementando un operador adecuado de conversión de tuplas . Las siguientes versiones fallan:
-
operator tuple<int,string>() const
-
operator tuple<const int&,const string&>() const
Dan como resultado un error en la asignación, diciendo que " operator =
no está sobrecargado para tuple<int&,string&> = Foo
". Supongo que esto se debe a que "la conversión a cualquier tipo X + parámetro de plantilla de deducción X para operator =" no funciona en conjunto, solo uno a la vez.
Intento imperfecto:
Por lo tanto, traté de implementar un operador de conversión para el tipo exacto del vínculo :
La asignación ahora funciona, ya que los tipos son ahora (después de la conversión) exactamente iguales, pero esto no funcionará en tres escenarios que me gustaría apoyar:
- Si el empate tiene variables de tipos diferentes pero convertibles vinculados (es decir, cambie
int a;
long long a;
en el lado del cliente), falla ya que los tipos tienen que coincidir completamente. Esto contradice el uso habitual de asignar una tupla a una tupla de referencias que permite tipos convertibles. (1) - El operador de conversión debe devolver un enlace al que se le deben otorgar referencias lvalue. Esto no funcionará para valores temporales o miembros de const. (2)
- Si el operador de conversión no es const, la asignación también falla para un
const Foo
en el lado derecho. Para implementar una versión constante de la conversión, debemos eliminar la constidad de los miembros del tema const. Esto es feo y puede ser abusado, lo que resulta en un comportamiento indefinido.
Solo veo una alternativa para proporcionar mi propia función de tie
+ clase junto con mis objetos "vinculables", lo que me obliga a duplicar la funcionalidad de std::tie
que no me gusta (no es que me resulte difícil hazlo, pero se siente mal tener que hacerlo).
Creo que al final del día, la conclusión es que este es un inconveniente de una implementación de tupla solo para bibliotecas. No son tan mágicos como nos gustaría que fueran.
EDITAR:
Como resultado, no parece haber una solución real que aborde todos los problemas anteriores. Una muy buena respuesta explicaría por qué esto no se puede resolver. En particular, me gustaría que alguien arroje algo de luz sobre por qué los "intentos fallidos" no pueden funcionar.
(1): Un hack horrible es escribir la conversión como una plantilla y convertirla a los tipos de miembros solicitados en el operador de conversión. Es un hack horrible porque no sé dónde almacenar estos miembros convertidos. En esta demostración , utilizo variables estáticas, pero esto no es reentrante de hilo.
(2): Se puede aplicar el mismo truco que en (1).
Como las otras respuestas ya explican, o bien debe heredar de una tuple
(para hacer coincidir la plantilla del operador de asignación) o convertir a la misma tuple
exacta de referencias (para hacer coincidir el operador de asignación sin plantillas tomando una tuple
de referencias de los mismos tipos).
Si heredaras de una tupla, foo.a
miembros nombrados, es decir, foo.a
ya no es posible.
En esta respuesta, presento otra opción: si está dispuesto a pagar una sobrecarga de espacio (constante por miembro) puede tener tanto miembros nombrados como una herencia de tuplas simultáneamente heredando de una tupla de referencias de const , es decir, una const vínculo del objeto en sí:
struct Foo : tuple<const int&, const string&> {
int a;
string b;
Foo(int a, string b) :
tuple{std::tie(this->a, this->b)},
a{a}, b{b}
{}
};
Este "vínculo adjunto" hace posible asignar un Foo
(no const!) A un empate de tipos de componentes convertibles. Dado que el "vínculo adjunto" es una tupla de referencias, automáticamente asigna los valores actuales de los miembros, aunque usted lo inicializó en el constructor.
¿Por qué es el const
"adjunto" const
? Porque de lo contrario, un const Foo
podría modificarse a través de su atadura adjunta.
Ejemplo de uso con tipos de componente no exactos del empate (tenga en cuenta el long long
vs int
):
int main()
{
Foo foo(0, "bar");
foo.a = 42;
long long a;
string b;
tie(a, b) = foo;
cout << a << '' '' << b << ''/n'';
}
imprimirá
42 bar
Entonces esto resuelve los problemas 1. + 3. al introducir un espacio en el techo.
Este tipo de hace lo que quiere, ¿verdad? (supone que sus valores pueden estar vinculados a los tipos de curso ...)
#include <tuple>
#include <string>
#include <iostream>
#include <functional>
using namespace std;
struct Foo {
int a;
string b;
template <template<typename ...Args> class tuple, typename ...Args>
operator tuple<Args...>() const {
return forward_as_tuple(get<Args>()...);
}
template <template<typename ...Args> class tuple, typename ...Args>
operator tuple<Args...>() {
return forward_as_tuple(get<Args>()...);
}
private:
// This is hacky, may be there is a way to avoid it...
template <typename T>
T get()
{ static typename remove_reference<T>::type i; return i; }
template <typename T>
T get() const
{ static typename remove_reference<T>::type i; return i; }
};
template <>
int&
Foo::get()
{ return a; }
template <>
string&
Foo::get()
{ return b; }
template <>
int&
Foo::get() const
{ return *const_cast<int*>(&a); }
template <>
string&
Foo::get() const
{ return *const_cast<string*>(&b); }
int main() {
Foo foo { 42, "bar" };
const Foo foo2 { 43, "gah" };
int a;
string b;
tie(a, b) = foo;
cout << a << ", " << b << endl;
tie(a, b) = foo2;
cout << a << ", " << b << endl;
}
La desventaja principal es que a cada miembro solo se puede acceder por sus tipos, ahora, podría solucionarlo con algún otro mecanismo (por ejemplo, definir un tipo por miembro y ajustar la referencia al tipo por el tipo de miembro que desea acceso..)
En segundo lugar, el operador de conversión no es explícito, se convertirá a cualquier tipo de tupla solicitado (puede ser que no desee eso ...)
La principal ventaja es que no tiene que especificar explícitamente el tipo de conversión, todo se deduce ...
Solo hay dos maneras en que puedes intentar ir:
- Utilice los operadores de asignación con plantillas:
Debe derivar públicamente de un tipo que el operador de asignación con plantilla coincida exactamente. - Use los operadores de asignación sin plantilla:
Ofrezca una conversión noexplicit
al tipo que el copiador sin plantilla espera, por lo que se usará. - No hay una tercera opción
En ambos casos, su tipo debe contener los elementos que desea asignar, no hay forma de evitarlo.
#include <iostream>
#include <tuple>
using namespace std;
struct X : tuple<int,int> {
};
struct Y {
int i;
operator tuple<int&,int&>() {return tuple<int&,int&>{i,i};}
};
int main()
{
int a, b;
tie(a, b) = make_tuple(9,9);
tie(a, b) = X{};
tie(a, b) = Y{};
cout << a << '' '' << b << ''/n'';
}
En coliru: http://coliru.stacked-crooked.com/a/315d4a43c62eec8d