Cómo sobrecargar std::swap()

Resuelto Adam asked hace 16 años • 4 respuestas

std::swap()Es utilizado por muchos contenedores estándar (como std::listy std::vector) durante la clasificación e incluso la asignación.

Pero la implementación estándar de swap()es muy generalizada y bastante ineficiente para tipos personalizados.

Por lo tanto, se puede ganar eficiencia sobrecargando std::swap()con una implementación específica de tipo personalizado. Pero, ¿cómo se puede implementar para que sea utilizado por los contenedores estándar?

Adam avatar Aug 15 '08 02:08 Adam
Aceptado

La forma correcta de sobrecargar std::swapla implementación de (también conocida como especializarla) es escribirla en el mismo espacio de nombres que lo que está intercambiando, para que pueda encontrarse mediante una búsqueda dependiente de argumentos (ADL) . Una cosa particularmente fácil de hacer es:

class X
{
    // ...
    friend void swap(X& a, X& b)
    {
        using std::swap; // bring in swap for built-in types

        swap(a.base1, b.base1);
        swap(a.base2, b.base2);
        // ...
        swap(a.member1, b.member1);
        swap(a.member2, b.member2);
        // ...
    }
};
Dave Abrahams avatar Apr 21 '2010 16:04 Dave Abrahams

Atención Mozza314

Aquí hay una simulación de los efectos de una std::algorithmllamada genérica std::swapy de que el usuario proporcione su intercambio en el espacio de nombres estándar. Como se trata de un experimento, esta simulación utiliza namespace expen lugar de namespace std.

// simulate <algorithm>

#include <cstdio>

namespace exp
{

    template <class T>
    void
    swap(T& x, T& y)
    {
        printf("generic exp::swap\n");
        T tmp = x;
        x = y;
        y = tmp;
    }

    template <class T>
    void algorithm(T* begin, T* end)
    {
        if (end-begin >= 2)
            exp::swap(begin[0], begin[1]);
    }

}

// simulate user code which includes <algorithm>

struct A
{
};

namespace exp
{
    void swap(A&, A&)
    {
        printf("exp::swap(A, A)\n");
    }

}

// exercise simulation

int main()
{
    A a[2];
    exp::algorithm(a, a+2);
}

Para mí esto imprime:

generic exp::swap

Si su compilador imprime algo diferente, entonces no está implementando correctamente la "búsqueda en dos fases" para las plantillas.

Si su compilador se ajusta (a cualquiera de C++ 98/03/11), dará el mismo resultado que muestro. Y en ese caso sucede exactamente lo que temes que suceda. Y poner su swapespacio de nombres std( exp) no impidió que sucediera.

Dave y yo somos miembros del comité y hemos estado trabajando en esta área del estándar durante una década (y no siempre de acuerdo entre nosotros). Pero esta cuestión se resolvió hace mucho tiempo y ambos coincidimos en cómo se resolvió. Ignore la opinión/respuesta experta de Dave en esta área bajo su propia responsabilidad.

Este problema salió a la luz después de la publicación de C++98. A partir del año 2001, Dave y yo comenzamos a trabajar en esta área . Y esta es la solución moderna:

// simulate <algorithm>

#include <cstdio>

namespace exp
{

    template <class T>
    void
    swap(T& x, T& y)
    {
        printf("generic exp::swap\n");
        T tmp = x;
        x = y;
        y = tmp;
    }

    template <class T>
    void algorithm(T* begin, T* end)
    {
        if (end-begin >= 2)
            swap(begin[0], begin[1]);
    }

}

// simulate user code which includes <algorithm>

struct A
{
};

void swap(A&, A&)
{
    printf("swap(A, A)\n");
}

// exercise simulation

int main()
{
    A a[2];
    exp::algorithm(a, a+2);
}

La salida es:

swap(A, A)

Actualizar

Se ha hecho una observación de que:

namespace exp
{    
    template <>
    void swap(A&, A&)
    {
        printf("exp::swap(A, A)\n");
    }

}

¡obras! Entonces, ¿por qué no usar eso?

Considere el caso de que Asea una plantilla de clase:

// simulate user code which includes <algorithm>

template <class T>
struct A
{
};

namespace exp
{

    template <class T>
    void swap(A<T>&, A<T>&)
    {
        printf("exp::swap(A, A)\n");
    }

}

// exercise simulation

int main()
{
    A<int> a[2];
    exp::algorithm(a, a+2);
}

Ahora no vuelve a funcionar. :-(

Entonces podrías poner swapel espacio de nombres estándar y hacer que funcione. Pero deberás recordar incluir swapel Aespacio de nombres de en el caso de que tengas una plantilla: A<T>. Y dado que ambos casos funcionarán si ingresa swapel Aespacio de nombres, es más fácil recordar (y enseñar a otros) hacerlo de esa manera.

Howard Hinnant avatar Dec 08 '2011 23:12 Howard Hinnant

No está permitido (según el estándar C++) sobrecargar std::swap, sin embargo, está específicamente permitido agregar especializaciones de plantilla para sus propios tipos al espacio de nombres estándar. P.ej

namespace std
{
    template<>
    void swap(my_type& lhs, my_type& rhs)
    {
       // ... blah
    }
}

entonces los usos en los contenedores estándar (y en cualquier otro lugar) elegirán su especialización en lugar de la general.

También tenga en cuenta que proporcionar una implementación de clase base de swap no es suficiente para sus tipos derivados. Por ejemplo, si tienes

class Base
{
    // ... stuff ...
}
class Derived : public Base
{
    // ... stuff ...
}

namespace std
{
    template<>
    void swap(Base& lha, Base& rhs)
    {
       // ...
    }
}

Esto funcionará para las clases base, pero si intenta intercambiar dos objetos derivados, usará la versión genérica de std porque el intercambio con plantilla coincide exactamente (y evita el problema de intercambiar solo las partes 'base' de sus objetos derivados). ).

NOTA: Actualicé esto para eliminar los bits incorrectos de mi última respuesta. ¡Oh! (gracias puetzk y j_random_hacker por señalarlo)

Wilka avatar Aug 14 '2008 19:08 Wilka

Si bien es correcto que generalmente no se deben agregar cosas al espacio de nombres std::, se permite específicamente agregar especializaciones de plantilla para tipos definidos por el usuario. La sobrecarga de funciones no lo es. Esta es una diferencia sutil :-)

17.4.3.1/1 No está definido que un programa C++ agregue declaraciones o definiciones al espacio de nombres std o espacios de nombres con el espacio de nombres std a menos que se especifique lo contrario. Un programa puede agregar especializaciones de plantilla para cualquier plantilla de biblioteca estándar al espacio de nombres std. Tal especialización (completa o parcial) de una biblioteca estándar da como resultado un comportamiento indefinido a menos que la declaración dependa de un nombre de enlace externo definido por el usuario y a menos que la especialización de la plantilla cumpla con los requisitos de la biblioteca estándar para la plantilla original.

Una especialización de std::swap se vería así:

namespace std
{
    template<>
    void swap(myspace::mytype& a, myspace::mytype& b) { ... }
}

Sin el bit plantilla<> sería una sobrecarga, que no está definida, en lugar de una especialización, que está permitida. El enfoque sugerido por @Wilka de cambiar el espacio de nombres predeterminado puede funcionar con el código de usuario (debido a que la búsqueda de Koenig prefiere la versión sin espacio de nombres), pero no está garantizado y, de hecho, no se supone que lo haga (la implementación STL debe usar el espacio de nombres completo). -std calificado::swap).

Hay un hilo en comp.lang.c++.moderado con una larga discusión sobre el tema. Sin embargo, la mayor parte se trata de especialización parcial (que actualmente no existe una buena manera de hacerlo).

puetzk avatar Sep 20 '2008 22:09 puetzk