función de miembro de intercambio de amigo público

169

En la hermosa respuesta al idioma de copiar e intercambiar hay un código que necesito un poco de ayuda:

class dumb_array
{
public:
    // ...
    friend void swap(dumb_array& first, dumb_array& second) // nothrow
    {
        using std::swap; 
        swap(first.mSize, second.mSize); 
        swap(first.mArray, second.mArray);
    }
    // ...
};

y agrega una nota

Hay otras afirmaciones de que deberíamos especializarnos en std :: swap para nuestro tipo, proporcionar un intercambio en clase junto con un intercambio de función libre, etc. Pero esto es innecesario: cualquier uso adecuado de swap se realizará a través de una llamada no calificada , y nuestra función se encontrará a través de ADL. Una función servirá.

Con friendque estoy un poco en términos "hostiles", debo admitirlo. Entonces, mis preguntas principales son:

  • parece una función libre , pero está dentro del cuerpo de la clase?
  • ¿Por qué no es esto swapestático ? Obviamente no usa ninguna variable miembro.
  • "¿Cualquier uso apropiado de swap lo descubrirá a través de ADL" ? ADL buscará los espacios de nombres, ¿verdad? ¿Pero también se ve dentro de las clases? ¿O es aquí donde friendentra?

Preguntas secundarias:

  • Con C ++ 11, ¿debo marcar mi swaps con noexcept?
  • Con C ++ 11 y su rango-para , debo colocar friend iter begin()y friend iter end()de la misma manera dentro de la clase? Creo que friendno se necesita aquí, ¿verdad?
Towi
fuente
Teniendo en cuenta la pregunta secundaria sobre el rango basado en: es mejor idea escribir funciones miembro y dejar el acceso al rango en begin () y end () en el espacio de nombres estándar (§24.6.5), basado en el rango para el uso interno de estos desde global o espacio de nombres estándar (ver §6.5.4). Sin embargo, tiene el inconveniente de que estas funciones son parte del encabezado <iterator>, si no lo incluye, es posible que desee escribirlas usted mismo.
Vitus
2
por qué no es estático, porque una friendfunción no es una función miembro en absoluto.
aschepler

Respuestas:

175

Hay varias formas de escribir swap, algunas mejores que otras. Sin embargo, con el tiempo, se descubrió que una sola definición funciona mejor. Consideremos cómo podríamos pensar en escribir una swapfunción.


Primero vemos que los contenedores como std::vector<>tienen una función miembro de argumento único swap, como:

struct vector
{
    void swap(vector&) { /* swap members */ }
};

Naturalmente, entonces, nuestra clase también debería, ¿verdad? Bueno en realidad no. La biblioteca estándar tiene todo tipo de cosas innecesarias , y un miembro swapes una de ellas. ¿Por qué? Continúemos.


Lo que debemos hacer es identificar qué es canónico y qué necesita hacer nuestra clase para trabajar con él. Y el método canónico de intercambio es con std::swap. Esta es la razón por la cual las funciones miembro no son útiles: no son cómo debemos intercambiar cosas, en general, y no influyen en el comportamiento de std::swap.

Bueno, para hacer el std::swaptrabajo deberíamos proporcionar (y std::vector<>deberíamos haber proporcionado) una especialización std::swap, ¿verdad?

namespace std
{
    template <> // important! specialization in std is OK, overloading is UB
    void swap(myclass&, myclass&)
    {
        // swap
    }
}

Bueno, eso ciertamente funcionaría en este caso, pero tiene un problema evidente: las especializaciones de funciones no pueden ser parciales. Es decir, no podemos especializar clases de plantillas con esto, solo instancias particulares:

namespace std
{
    template <typename T>
    void swap<T>(myclass<T>&, myclass<T>&) // error! no partial specialization
    {
        // swap
    }
}

Este método funciona algunas veces, pero no todo el tiempo. Debe haber una mejor manera.


¡Ahi esta! Podemos usar una friendfunción y encontrarla a través de ADL :

namespace xyz
{
    struct myclass
    {
        friend void swap(myclass&, myclass&);
    };
}

Cuando queremos intercambiar algo, asociamos std::swap y luego hacemos una llamada no calificada:

using std::swap; // allow use of std::swap...
swap(x, y); // ...but select overloads, first

// that is, if swap(x, y) finds a better match, via ADL, it
// will use that instead; otherwise it falls back to std::swap

¿Qué es una friendfunción? Hay confusión en esta área.

Antes de que C ++ se estandarizara, las friendfunciones hicieron algo llamado "inyección de nombre de amigo", donde el código se comportó como si la función se hubiera escrito en el espacio de nombres circundante. Por ejemplo, estos fueron pre-estándar equivalentes:

struct foo
{
    friend void bar()
    {
        // baz
    }
};

// turned into, pre-standard:    

struct foo
{
    friend void bar();
};

void bar()
{
    // baz
}

Sin embargo, cuando se inventó ADL, esto se eliminó. La friendfunción solo se puede encontrar a través de ADL; si lo deseaba como una función libre, debía declararse así ( consulte esto , por ejemplo). Pero he aquí! Había un problema.

Si solo usa std::swap(x, y), su sobrecarga nunca será encontrada, porque ha dicho explícitamente "¡mire adentro std, y en ningún otro lugar"! Es por eso que algunas personas sugirieron escribir dos funciones: una como una función que se encuentra a través de ADL , y la otra para manejar std::calificaciones explícitas .

Pero como vimos, esto no puede funcionar en todos los casos, y terminamos con un desastre feo. En cambio, el intercambio idiomático fue por la otra ruta: en lugar de hacer que el trabajo de las clases sea el de proporcionar std::swap, es el trabajo de los intercambiadores asegurarse de que no usen calificados swap, como se indicó anteriormente. Y esto tiende a funcionar bastante bien, siempre y cuando la gente lo sepa. Pero ahí está el problema: ¡no es intuitivo necesitar usar una llamada no calificada!

Para facilitar esto, algunas bibliotecas como Boost proporcionaron la función boost::swap, que solo realiza una llamada no calificada swap, std::swapcomo un espacio de nombres asociado. Esto ayuda a que las cosas vuelvan a ser concisas, pero sigue siendo un fastidio.

Tenga en cuenta que no hay ningún cambio en el comportamiento de C ++ 11 std::swap, lo que yo y otros pensamos erróneamente que sería el caso. Si te mordió esto, lee aquí .


En resumen: la función miembro es solo ruido, la especialización es fea e incompleta, pero la friendfunción está completa y funciona. Y cuando intercambie, use boost::swapo no calificado swapcon std::swapasociado.


† Informalmente, se asocia un nombre si se considerará durante una llamada de función. Para los detalles, lea §3.4.2. En este caso, std::swapnormalmente no se considera; pero podemos asociarlo (agregarlo al conjunto de sobrecargas consideradas por no calificado swap), permitiendo que se encuentre.

GManNickG
fuente
10
No estoy de acuerdo con que la función miembro sea solo ruido. Una función miembro permite, por ejemplo std::vector<std::string>().swap(someVecWithData);, lo que no es posible con una swapfunción libre porque ambos argumentos se pasan por referencia no constante.
ildjarn
3
@ildjarn: Puedes hacerlo en dos líneas. Tener la función miembro viola el principio DRY.
GManNickG
44
@GMan: El principio DRY no se aplica si uno se implementa en términos del otro. De lo contrario, nadie abogaría por una clase con implementaciones de operator=, operator+y operator+=, pero claramente se acepta / espera que existan aquellos operadores en clases relevantes por simetría. Lo mismo se aplica a member swap+ namespace-scoped swapen mi opinión.
ildjarn
3
@GMan Creo que está considerando demasiadas funciones. Poco conocido, pero incluso un function<void(A*)> f; if(!f) { }puede fallar solo porque Adeclara un operator!que acepta figual de bien que fel suyo operator!(poco probable, pero puede suceder). Si function<>el autor pensara "ohh tengo un 'operador bool', ¿por qué debería implementar 'operador!' ¡Eso violaría DRY!", Eso sería fatal. Solo necesita tener operator!implementado Ay Atener un constructor para a function<...>, y las cosas se romperán, porque ambos candidatos requerirán conversiones definidas por el usuario.
Johannes Schaub - litb
1
Consideremos cómo podríamos pensar en escribir una función de intercambio [miembro]. Naturalmente, entonces, nuestra clase también debería, ¿verdad? Bueno en realidad no. La biblioteca estándar tiene todo tipo de cosas innecesarias , y un intercambio de miembros es una de ellas. El GotW vinculado aboga por la función de intercambio de miembros.
Xeverous
7

Ese código es equivalente (en casi todos los sentidos) a:

class dumb_array
{
public:
    // ...
    friend void swap(dumb_array& first, dumb_array& second);
    // ...
};

inline void swap(dumb_array& first, dumb_array& second) // nothrow
{
    using std::swap; 
    swap(first.mSize, second.mSize); 
    swap(first.mArray, second.mArray);
}

Una función amiga definida dentro de una clase es:

  • colocado en el espacio de nombres adjunto
  • automáticamente inline
  • capaz de referirse a miembros estáticos de la clase sin calificación adicional

Las reglas exactas están en la sección [class.friend](cito los párrafos 6 y 7 del borrador de C ++ 0x):

Una función se puede definir en una declaración amiga de una clase si y solo si la clase es una clase no local (9.8), el nombre de la función no está calificado y la función tiene un alcance de espacio de nombres.

Tal función está implícitamente en línea. Una función amiga definida en una clase está en el alcance (léxico) de la clase en la que se define. Una función amiga definida fuera de la clase no lo es.

Ben Voigt
fuente
2
En realidad, las funciones de amigo no se colocan en el espacio de nombres adjunto, en C ++ estándar. El antiguo comportamiento se llamaba "inyección de nombre de amigo", pero fue reemplazado por ADL, reemplazado en el primer estándar. Vea la parte superior de esto . (Sin embargo, el comportamiento es bastante similar.)
GManNickG
1
No es realmente equivalente. El código en la pregunta hace que swapsolo sea visible para ADL. Es un miembro del espacio de nombres adjunto, pero su nombre no es visible para los otros formularios de búsqueda de nombres. EDITAR: veo que @GMan fue más rápido otra vez :) @Ben siempre ha sido así en ISO C ++ :)
Johannes Schaub - litb
2
@Ben: No, la inyección de amigos nunca existió en un estándar, pero antes se usaba ampliamente, por lo que la idea (y el soporte del compilador) tendió a continuar, pero técnicamente no existe. friendADL solo encuentra funciones, y si solo necesitan ser funciones libres con friendacceso, deben declararse como frienddentro de la clase y como una declaración de función libre normal fuera de la clase. Puede ver esa necesidad en esta respuesta , por ejemplo.
GManNickG
2
@towi: Debido a que la función de amigo está en el ámbito del espacio de nombres, las respuestas a las tres preguntas deben quedar claras: (1) Es una función gratuita, además tiene acceso de amigo a miembros privados y protegidos de la clase. (2) No es miembro en absoluto, ni instancia ni estático. (3) ADL no busca dentro de las clases, pero esto está bien porque la función amigo tiene un alcance de espacio de nombres.
Ben Voigt
1
@Ben. En la especificación, la función es un miembro de espacio de nombres, y la frase "la función tiene alcance de espacio de nombres" puede interpretarse para decir que la función es un miembro de espacio de nombres (depende en gran medida del contexto de dicha declaración). Y agrega un nombre a ese espacio de nombres que solo es visible para ADL (en realidad, IIRC parte contradice otras partes en la especificación sobre si se agrega o no algún nombre. Pero la adición de un nombre es necesaria para detectar declaraciones incompatibles agregadas a ese espacio de nombres, por lo que, de hecho, se agrega un nombre invisible . Consulte la nota en 3.3.1p4).
Johannes Schaub - litb