Hacer tipo personalizado " tie-able "(compatible con std:: tie)


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 sea asignable a un std::tie, es decir, std::tuple de referencias?

Foo foo = ...;

int a;
string b;

std::tie(a, b) = foo;

Intentos Fallidos:

No es posible sobrecargar el operador de asignación para tuple<int&,string&> = Foo, ya que el operador de asignación es uno de los operadores binarios que tienen que ser miembros del objeto del lado izquierdo.

Así que intenté resolver esto implementando una conversión de tupla adecuada operador. Las siguientes versiones fallan:

  • operator tuple<int,string>() const
  • operator tuple<const int&,const string&>() const

Resultan en un error en la asignación, diciendo que "operator = no está sobrecargado para tuple<int&,string&> = Foo". Supongo que esto se debe a que "conversión a cualquier tipo X + deducir parámetro de plantilla X para operador=" no funcionan juntos, solo uno de ellos a la vez.

Intento imperfecto:

Por lo tanto, traté de implementar un operador de conversión para el tipo exacto de la tie :

  • operator tuple<int&,string&>() const Demo
  • operator tuple<int&,string&>() Demo

La asignación ahora funciona ya que los tipos son ahora (después de la conversión) exactamente los mismos, pero esto no funcionará para tres escenarios que me gustaría soportar:

  1. Si el empate tiene variables de tipos diferentes pero convertibles enlazadas (es decir, cambiar int a; 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)
  2. El operador de conversión necesita devolver un tie al que se le deben dar referencias lvalue. Esto no funcionará para los valores temporales o los miembros de const.(2)
  3. 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 const de la conversión, necesitamos hackear la constancia de la miembros del tema const. Esto es feo y podría ser abusado, lo que resulta en un comportamiento indefinido.

Solo veo una alternativa al proporcionar mi propia tie función + clase junto con mis objetos "tie-able", lo que me obliga a duplicar la funcionalidad de std::tie que no me gusta (no es que me resulte difícil hacerlo, 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 tupla solo para bibliotecas aplicación. No son tan mágicos como nos gustaría que fueran.

EDITAR:

Resulta que no parece haber una solución real que aborde todos los problemas anteriores. Una respuesta muy buena explicaría por qué esto no es solucionable. En particular, me gustaría que alguien arrojara algo de luz sobre por qué los "intentos fallidos" no pueden funcionar.


(1): Un truco horrible es escribir la conversión como una plantilla y convertir a los tipos de miembros solicitados en el operador de conversión. Es un hack horrible porque no se donde almacenar estos miembros convertidos. En esta demo uso variables estáticas, pero esto no es reentrante de hilo.

(2): Se puede aplicar el mismo truco que en (1).

Author: leemes, 2014-10-09

4 answers

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 tienen varios operadores de asignación:

  • Un operador de asignación predeterminado, que toma un std::tuple<T...>
  • Un operador de asignación de conversión de tupla plantilla con un paquete de parámetros de tipo U..., que toma un std::tuple<U...>
  • Un operador de asignación de conversión de pares plantilla con dos parámetros de tipo U1, U2, std::pair<U1, U2>

Para esas tres versiones existen variantes de copia y movimiento; agregue un const& o un && a los tipos que toman.

Las plantillas de operador de asignación tienen que deducir sus argumentos de plantilla del tipo de argumento de función (es decir, del tipo del RHS de la expresión de asignación).

Sin un operador de conversión en Foo, ninguno de esos operadores de asignación son viables para std::tie(a,b) = foo. Si agrega un operador de conversión a Foo, entonces solo el valor predeterminado asignación-operador se vuelve viable: La deducción de tipo de plantilla no tiene en cuenta las conversiones definidas por el usuario. Es decir, no puede deducir argumentos de plantilla para las plantillas de operador de asignación del tipo Foo.

Dado que solo se permite una conversión definida por el usuario en una secuencia de conversión implícita, el tipo al que el operador de conversión convierte debe coincidir exactamente con el tipo del 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 conversiones de los tipos de elemento (por ejemplo, asignación de Foo::a a un 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 para las conversiones dentro de la plantilla del operador son bastante limitadas (montón, tipo juego de palabras, estático, hilo local, etc.).

 18
Author: dyp,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-10-09 12:30:34

Solo hay dos maneras de intentar ir:

  1. Utilice los operadores de asignación de plantilla:
    Debe derivar públicamente de un tipo el operador de asignación con plantilla coincide exactamente.
  2. Utilice los operadores de asignación no templados:
    Ofrezca una conversión no-explicit al tipo que espera el operador de copia no templada, por lo que se utilizará.
  3. No hay una tercera opción.

En ambos casos, su tipo debe contener los elementos que desea asignar, de ninguna manera rodean.

#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

 6
Author: Deduplicator,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-10-09 12:56:16

Como las otras respuestas ya explican, o bien tiene que heredar de un tuple (para que coincida con la plantilla del operador de asignación) o convertir a la misma tuple de referencias (para que coincida con el operador de asignación no templada tomando un tuple de referencias del mismo tipo).

Si heredaras de una tupla, perderías los miembros nombrados, es decir, foo.a ya no es posible.

En esta respuesta, presento otra opción: Si estás dispuesto a pagar por un espacio overhead (constante por miembro) puede tener ambos miembros nombrados así como una herencia tupla simultáneamente heredando de una tupla de referencias const, es decir, un lazo const del objeto mismo: {[13]]}

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}
    {}
};

Esta "atada" hace posible asignar un (non-const!) Foo a una vinculación de tipos de componentes convertibles. Dado que el "ated tie" es una tupla de referencias, asigna automáticamente los valores actuales de los miembros, aunque inicializado en el constructor.

¿Por qué la "corbata atada" const? Porque de lo contrario, un const Foo podría modificarse a través de su lazo adjunto.

Ejemplo de uso con tipos de componentes no exactos de la vinculación (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';
}

Se imprimirá

42 bar

Demostración en Vivo

Así que esto resuelve los problemas 1. + 3. mediante la introducción de un espacio de arriba.

 4
Author: leemes,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-10-09 13:26:05

Este tipo de hace lo que quieres bien? (asume que sus valores se pueden vincular 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;

}

El mayor inconveniente es que solo se puede acceder a cada miembro por sus tipos, ahora, podría evitar esto con algún otro mecanismo (por ejemplo, definir un tipo por miembro y envolver la referencia al tipo por el tipo de miembro al que desea acceder..)

En segundo lugar, el operador de conversión no es explícito, se convertirá a cualquier tipo de tupla sé que no quieres eso..)

La principal ventaja es que no tiene que especificar explícitamente el tipo de conversión, todo se deduce...

 2
Author: Nim,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-10-09 13:18:42