função de membro de troca de amigo público

169

Na bela resposta ao idioma de copiar e trocar, há um código que eu preciso de ajuda:

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);
    }
    // ...
};

e ele adiciona uma nota

Há outras alegações de que devemos especializar std :: swap para o nosso tipo, fornecer uma troca em classe ao lado de uma troca de função livre, etc. Mas tudo isso é desnecessário: qualquer uso adequado de troca será por meio de uma chamada não qualificada , e nossa função será encontrada através da ADL. Uma função serve.

Com friendeu sou um pouco em termos "hostis", devo admitir. Então, minhas principais perguntas são:

  • parece uma função livre , mas está dentro do corpo da classe?
  • por que isso não é swapestático ? Obviamente, ele não usa nenhuma variável de membro.
  • "Qualquer uso adequado do swap descobrirá o swap via ADL" ? A ADL pesquisará os namespaces, certo? Mas também olha dentro das classes? Ou é aqui que friendentra?

Perguntas secundárias:

  • Com o C ++ 11, devo marcar meus swaps com noexcept?
  • Com C ++ 11 e sua gama-for , eu deveria colocar friend iter begin()e friend iter end()da mesma forma dentro da classe? Eu acho que friendnão é necessário aqui, certo?
towi
fonte
Considerando a questão secundária sobre o intervalo para: é melhor escrever funções-membro e deixar o acesso ao intervalo em begin () e end () no espaço de nomes std (§24.6.5), baseado em intervalo para utilizá-los internamente, de fontes globais ou globais. namespace padrão (consulte §6.5.4). No entanto, o lado negativo é que essas funções fazem parte do cabeçalho <iterator>; se você não o incluir, convém escrevê-las.
Vitus #
2
por que não é estático - porque uma friendfunção não é uma função membro?
aschepler

Respostas:

175

Existem várias maneiras de escrever swap, algumas melhores que outras. Com o tempo, porém, verificou-se que uma única definição funciona melhor. Vamos considerar como podemos pensar em escrever uma swapfunção.


Primeiro, vemos que contêineres std::vector<>possuem uma função de membro de argumento único swap, como:

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

Naturalmente, então, nossa classe também deveria, certo? Bem, na verdade não. A biblioteca padrão tem todo tipo de coisas desnecessárias e um membro swapé um deles. Por quê? Vamos continuar.


O que devemos fazer é identificar o que é canônico e o que nossa classe precisa fazer para trabalhar com ela. E o método canônico de troca é com std::swap. É por isso que as funções membro não são úteis: elas não são como devemos trocar as coisas, em geral, e não têm influência no comportamento de std::swap.

Bem, então, para fazer o std::swaptrabalho, devemos fornecer (e std::vector<>deveria ter fornecido) uma especialização std::swap, certo?

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

Bem, isso certamente funcionaria neste caso, mas tem um problema evidente: as especializações de funções não podem ser parciais. Ou seja, não podemos especializar classes de modelo com isso, apenas instanciações específicas:

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

Esse método funciona algumas vezes, mas não o tempo todo. Deve haver uma maneira melhor.


Há sim! Podemos usar uma friendfunção e encontrá-la através da ADL :

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

Quando queremos trocar alguma coisa, associamos std::swap e, em seguida, fazemos uma chamada não qualificada:

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

O que é uma friendfunção? Há confusão nessa área.

Antes da padronização do C ++, as friendfunções executavam algo chamado "injeção de nome de amigo", em que o código se comportava como se a função tivesse sido escrita no espaço para nome ao redor. Por exemplo, esses eram pré-padrão equivalente:

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

// turned into, pre-standard:    

struct foo
{
    friend void bar();
};

void bar()
{
    // baz
}

No entanto, quando a ADL foi inventada, essa foi removida. A friendfunção poderia ser encontrada via ADL; se você a quisesse como uma função livre, ela precisava ser declarada como tal ( veja isso , por exemplo). Mas eis! Havia um problema.

Se você apenas usar std::swap(x, y), sua sobrecarga nunca será encontrada, porque você disse explicitamente "procure dentro stde em nenhum outro lugar"! É por isso que algumas pessoas sugeriram escrever duas funções: uma como uma função que pode ser encontrada via ADL e a outra para lidar com std::qualificações explícitas .

Mas, como vimos, isso não pode funcionar em todos os casos, e acabamos com uma bagunça feia. Em vez disso, a troca idiomática seguiu o outro caminho: em vez de tornar o trabalho das classes fornecer std::swap, é trabalho dos trocadores garantir que eles não usem qualificada swap, como acima. E isso tende a funcionar muito bem, desde que as pessoas saibam disso. Mas aí está o problema: não é intuitivo precisar usar uma chamada não qualificada!

Para facilitar isso, algumas bibliotecas como o Boost forneceram a função boost::swap , que apenas faz uma chamada não qualificada swap, com std::swapum espaço de nome associado. Isso ajuda a tornar as coisas sucintas novamente, mas ainda é uma chatice.

Observe que não há alteração no C ++ 11 no comportamento de std::swap , que eu e outros pensamos erroneamente que seria o caso. Se você foi mordido por isso, leia aqui .


Em resumo: a função membro é apenas ruído, a especialização é feia e incompleta, mas a friendfunção está completa e funciona. E quando você trocar, use boost::swapou um não qualificadoswap com std::swapassociado.


† Informalmente, um nome é associado se for considerado durante uma chamada de função. Para detalhes, leia §3.4.2. Nesse caso, std::swapnormalmente não é considerado; mas podemos associá- lo (adicione-o ao conjunto de sobrecargas consideradas não qualificadas swap), permitindo que seja encontrado.

GManNickG
fonte
10
Não concordo que a função de membro seja apenas ruído. Uma função membro permite, por exemplo std::vector<std::string>().swap(someVecWithData);, o que não é possível com uma swapfunção livre porque os dois argumentos são passados ​​por referência não const.
precisa saber é o seguinte
3
@ildjarn: Você pode fazer isso em duas linhas. Ter a função de membro viola o princípio DRY.
precisa saber é o seguinte
4
@ GMan: O princípio DRY não se aplica se um for implementado em termos do outro. Caso contrário, ninguém iria defender uma classe com implementações de operator=, operator+e operator+=, mas é evidente que os operadores em classes relevantes são aceitos / esperados de existir para simetria. O mesmo vale para o swapescopo de membro + espaço para nome swapna minha opinião.
Ildjarn
3
@ Cara, acho que está considerando muitas funções. Pouco conhecido, mas mesmo um function<void(A*)> f; if(!f) { }pode falhar apenas porque Adeclara um operator!que aceita figualmente bem fo próprio operator!(improvável, mas pode acontecer). Se function<>o autor pensasse "ohh, eu tenho um 'operador bool' ', por que eu deveria implementar' operator! '? Isso violaria DRY!", Isso seria fatal. Você só precisa ter um operator!implementado para A, e Ater um construtor para a function<...>, e as coisas vão quebrar, porque os dois candidatos exigirão conversões definidas pelo usuário.
Johannes Schaub - litb
1
Vamos considerar como podemos pensar em escrever uma função de troca [membro]. Naturalmente, então, nossa classe também deveria, certo? Bem, na verdade não. A biblioteca padrão tem todo tipo de coisas desnecessárias , e uma troca de membros é uma delas. O GotW vinculado defende a função de troca de membro.
Xeverous
7

Esse código é equivalente (em quase todos os aspectos) 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);
}

Uma função de amigo definida dentro de uma classe é:

  • colocado no espaço para nome anexo
  • automaticamente inline
  • capaz de se referir a membros estáticos da classe sem qualificação adicional

As regras exatas estão na seção [class.friend](cito os parágrafos 6 e 7 do rascunho de C ++ 0x):

Uma função pode ser definida em uma declaração de amigo de uma classe se, e somente se a classe for uma classe não local (9.8), o nome da função não for qualificado e a função tiver escopo de espaço para nome.

Essa função está implicitamente embutida. Uma função de amigo definida em uma classe está no escopo (lexical) da classe em que está definida. Uma função de amigo definida fora da classe não é.

Ben Voigt
fonte
2
Na verdade, as funções de amigo não são colocadas no espaço para nome anexo, no C ++ padrão. O antigo comportamento foi chamado de "injeção de nome de amigo", mas foi substituído pelo ADL, substituído no primeiro padrão. Veja o topo disso . (O comportamento é bastante semelhante, apesar de tudo.)
GManNickG
1
Não é realmente equivalente. O código na pergunta torna swapvisível apenas para ADL. É um membro do espaço para nome anexo, mas seu nome não é visível para os outros formulários de pesquisa de nome. EDIT: Eu vejo que @GMan foi mais rápido novamente :) @ Ben sempre foi assim no ISO C ++ :)
Johannes Schaub - litb
2
@ Ben: Não, a injeção de amigos nunca existiu em um padrão, mas foi amplamente usada antes e é por isso que a idéia (e o suporte ao compilador) tendem a continuar, mas tecnicamente não existe. friendfunções são encontradas apenas pela ADL e, se elas precisam ser apenas funções livres com friendacesso, elas precisam ser declaradas como frienddentro da classe e como uma declaração de função livre normal fora da classe. Você pode ver essa necessidade nesta resposta , por exemplo.
precisa saber é o seguinte
2
@towi: Como a função de amigo está no escopo do namespace, as respostas para todas as suas três perguntas devem ficar claras: (1) É uma função gratuita, além de ter acesso de amigo a membros privados e protegidos da classe. (2) Não é um membro, nem instância nem estática. (3) A ADL não pesquisa dentro das classes, mas tudo bem, porque a função friend possui escopo de espaço para nome.
Ben Voigt
1
@Ben. Na especificação, a função é um membro do espaço para nome e a frase "a função tem escopo para espaço para nome" pode ser interpretada para dizer que a função é um membro do espaço para nome (depende muito do contexto de uma declaração). E adiciona um nome ao namespace que é visível apenas para a ADL (na verdade, algumas partes do IIRC contradizem outras partes na especificação sobre a adição ou não de um nome. Mas a adição de um nome é necessária para detectar declarações incompatíveis adicionadas a ele namespace; portanto, um nome invisível é adicionado.Veja a nota em 3.3.1p4).
Johannes Schaub - litb