Cómo sobrecargar std :: swap ()

115

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 los tipos personalizados.

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

Adán
fuente
Información relacionada: en.cppreference.com/w/cpp/concept/Swappable
Pharap
La página Swappable se movió a en.cppreference.com/w/cpp/named_req/Swappable
Andrew Keeton

Respuestas:

135

La forma correcta de sobrecargar el intercambio es escribirlo en el mismo espacio de nombres que lo que está intercambiando, para que se pueda encontrar mediante la 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
fuente
11
En C ++ 2003, en el mejor de los casos, está subespecificado. La mayoría de las implementaciones usan ADL para encontrar el intercambio, pero no, no es obligatorio, por lo que no puede contar con él. Usted puede especializarse std :: intercambio para un tipo específico concreto, como se muestra por la OP; simplemente no espere que se utilice esa especialización, por ejemplo, para clases derivadas de ese tipo.
Dave Abrahams
15
Me sorprendería descubrir que las implementaciones aún no usan ADL para encontrar el intercambio correcto. Este es un tema antiguo en el comité. Si su implementación no usa ADL para encontrar el intercambio, presente un informe de error.
Howard Hinnant
3
@Sascha: Primero, estoy definiendo la función en el ámbito del espacio de nombres porque ese es el único tipo de definición que importa para el código genérico. Porque int et. Alabama. no / no puedo tener funciones miembro, std :: sort et. Alabama. tiene que usar una función gratuita; ellos establecen el protocolo. En segundo lugar, no sé por qué se opone a tener dos implementaciones, pero la mayoría de las clases están condenadas a ser ordenadas de manera ineficiente si no puede aceptar tener un intercambio de no miembros. Las reglas de sobrecarga aseguran que si se ven ambas declaraciones, se elegirá la más específica (esta) cuando se llame al swap sin calificación.
Dave Abrahams
5
@ Mozza314: Depende. A std::sortque usa ADL para intercambiar elementos es C ++ 03 no conforme pero conforme a C ++ 11. Además, ¿por qué -1 una respuesta basada en el hecho de que los clientes pueden usar código no idiomático?
JoeG
4
@curiousguy: Si leer el estándar fuera una simple cuestión de leer el estándar, tendrías razón :-). Desafortunadamente, la intención de los autores es importante. Entonces, si la intención original era que ADL podría o debería usarse, no está especificado. Si no es así, entonces es simplemente un viejo cambio rotundo para C ++ 0x, razón por la cual escribí "en el mejor de los casos" subespecificado.
Dave Abrahams
70

Atención Mozza314

Aquí hay una simulación de los efectos de una std::algorithmllamada genérica std::swap, y el usuario proporciona su intercambio en el espacio de nombres std. Como se trata de un experimento, esta simulación utiliza en namespace explugar 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 es conforme (a cualquiera de C ++ 98/03/11), entonces dará el mismo resultado que muestro. Y en ese caso, sucede exactamente lo que temes que suceda. Y poner su swapen el espacio de nombres std( exp) no impidió que sucediera.

Dave y yo somos miembros del comité y hemos trabajado en esta área de la norma durante una década (y no siempre de acuerdo entre nosotros). Pero este tema se ha resuelto durante mucho tiempo y ambos estamos de acuerdo en cómo se ha resuelto. Ignore la opinión / respuesta experta de Dave en esta área bajo su propio riesgo.

Este problema salió a la luz después de la publicación de C ++ 98. Aproximadamente en 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 que:

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

}

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

Considere el caso de que su Aes 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ía poner swapen el espacio de nombres std y hacer que funcione. Sin embargo, usted tiene que recordar para poner swapen Aespacio de nombres 's para el caso cuando se tiene una plantilla: A<T>. Y dado que ambos casos funcionarán si pones swapen Ael espacio de nombres, es más fácil recordar (y enseñar a otros) que lo hagan de una manera.

Howard Hinnant
fuente
4
Muchas gracias por la respuesta detallada. Claramente, tengo menos conocimientos sobre esto y en realidad me preguntaba cómo la sobrecarga y la especialización podrían producir comportamientos diferentes. Sin embargo, no estoy sugiriendo sobrecarga sino especialización. Cuando pongo template <>su primer ejemplo, obtengo una salida exp::swap(A, A)de gcc. Entonces, ¿por qué no preferir la especialización?
voltrevo
1
¡Guauu! Esto es realmente esclarecedor. Definitivamente me has convencido. Creo que modificaré ligeramente tu sugerencia y usaré la sintaxis de amigo en clase de Dave Abrahams (¡oye, puedo usar esto para el operador << también! :-)), a menos que tengas una razón para evitar eso también (aparte de compilar por separado). Además, a la luz de esto, ¿cree que using std::swapes una excepción a la regla de "nunca poner usando instrucciones dentro de los archivos de encabezado"? De hecho, ¿por qué no ponerlo using std::swapdentro <algorithm>? Supongo que podría romper una pequeña minoría del código de las personas. ¿Quizás desaprobar el apoyo y eventualmente ponerlo?
voltrevo
3
La sintaxis de amigo en clase debería estar bien. Intentaría limitar el using std::swapalcance de la función dentro de sus encabezados. Sí, swapes casi una palabra clave. Pero no, no es una palabra clave. Así que es mejor no exportarlo a todos los espacios de nombres hasta que realmente sea necesario. swapes muy parecido operator==. La mayor diferencia es que nadie piensa en llamar operator==con una sintaxis de espacio de nombres calificada (sería demasiado feo).
Howard Hinnant
15
@NielKirk: Lo que está viendo como una complicación es simplemente demasiadas respuestas incorrectas. No hay nada complicado en la respuesta correcta de Dave Abrahams: "La forma correcta de sobrecargar el intercambio es escribirlo en el mismo espacio de nombres que lo que está intercambiando, de modo que se pueda encontrar mediante la búsqueda dependiente de argumentos (ADL)".
Howard Hinnant
2
@codeshot: Lo siento. Herb ha estado tratando de transmitir este mensaje desde 1998: gotw.ca/publications/mill02.htm No menciona el intercambio en este artículo. Pero esta es solo otra aplicación del principio de interfaz de Herb.
Howard Hinnant
53

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

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

luego, 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 intercambio no es lo suficientemente bueno 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 es una coincidencia exacta (y evita el problema de intercambiar solo las partes 'base' de sus objetos derivados ).

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

Wilka
fuente
1
Principalmente un buen consejo, pero tengo que -1 debido a la sutil distinción señalada por puetzk entre especializar una plantilla en el espacio de nombres estándar (que está permitido por el estándar C ++) y sobrecargar (que no lo es).
j_random_hacker
11
Votado en contra porque la forma correcta de personalizar el intercambio es hacerlo en su propio espacio de nombres (como señala Dave Abrahams en otra respuesta).
Howard Hinnant
2
Mis razones para votar negativamente son las mismas que las de Howard
Dave Abrahams
13
@HowardHinnant, Dave Abrahams: No estoy de acuerdo. ¿Sobre qué base afirma que su alternativa es la forma "correcta"? Como puetzk citó de la norma, esto está específicamente permitido. Si bien soy nuevo en este tema, realmente no me gusta el método que defiende porque si defino Foo e intercambio de esa manera, es probable que otra persona que use mi código use std :: swap (a, b) en lugar de swap ( a, b) en Foo, que usa silenciosamente la ineficiente versión predeterminada.
voltrevo
5
@ Mozza314: Las limitaciones de espacio y formato del área de comentarios no me permitieron responderte completamente. Consulte la respuesta que agregué titulada "Atención Mozza314".
Howard Hinnant
29

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. Sobrecargar las funciones no lo es. Esta es una diferencia sutil :-)

17.4.3.1/1 No está definido para un programa C ++ agregar declaraciones o definiciones al espacio de nombres std o espacios de nombres con 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 definido por el usuario de enlace externo 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 la plantilla <> bit, sería una sobrecarga, que no está definida, en lugar de una especialización, que está permitida. El enfoque sugerido de @ Wilka para 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 de STL debería usar el -std :: swap calificado).

Hay un hilo en comp.lang.c ++. Moderado con una larga discusión del tema. Sin embargo, la mayor parte tiene que ver con la especialización parcial (que actualmente no hay una buena manera de hacerlo).

puetzk
fuente
7
Una de las razones por las que es incorrecto usar la especialización de plantilla de función para esto (o cualquier cosa): interactúa de mala manera con las sobrecargas, de las cuales hay muchas para el intercambio. Por ejemplo, si se especializa el estándar std :: swap por std :: vector <mytype> &, su especialización no será elegida sobre el swap específico de vector del estándar, porque las especializaciones no se consideran durante la resolución de sobrecarga.
Dave Abrahams
4
Esto también es lo que recomienda Meyers en Effective C ++ 3ed (artículo 25, págs. 106-112).
jww
2
@DaveAbrahams: Si se especializa (sin argumentos de plantilla explícitos), el orden parcial hará que sea una especialización de la vectorversión y se utilizará .
Davis Herring
1
@DavisHerring en realidad, no, cuando lo haces, el pedido parcial no juega ningún papel. El problema no es que no puedas llamarlo en absoluto; es lo que sucede en presencia de sobrecargas de intercambio aparentemente menos específicas: wandbox.org/permlink/nck8BkG0WPlRtavV
Dave Abrahams
2
@DaveAbrahams: El orden parcial es seleccionar la plantilla de función para especializarse cuando la especialización explícita coincide con más de una. La ::swapsobrecarga que agregó es más especializada que la std::swapsobrecarga vector, por lo que captura la llamada y ninguna especialización de esta última es relevante. No estoy seguro de cómo es un problema práctico (¡pero tampoco estoy afirmando que sea una buena idea!).
Davis Herring