Como sobrecarregar std :: swap ()

115

std::swap()é usado por muitos contêineres std (como std::liste std::vector) durante a classificação e até mesmo a atribuição.

Mas a implementação padrão do swap()é muito generalizada e bastante ineficiente para tipos personalizados.

Assim, a eficiência pode ser obtida sobrecarregando std::swap()com uma implementação específica de tipo personalizado. Mas como você pode implementá-lo para que seja usado pelos contêineres std?

Adão
fonte
Informações relacionadas: en.cppreference.com/w/cpp/concept/Swappable
Pharap
A página Swappable foi movida para en.cppreference.com/w/cpp/named_req/Swappable
Andrew Keeton

Respostas:

135

A maneira certa de sobrecarregar a troca é gravá-la no mesmo namespace do que você está trocando, para que possa ser encontrada por meio de pesquisa dependente de argumento (ADL) . Uma coisa particularmente fácil de fazer é:

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
fonte
11
Em C ++ 2003, ele é, na melhor das hipóteses, não especificado. A maioria das implementações usa ADL para localizar swap, mas não, não é obrigatório, então você não pode contar com isso. Você pode especializar std :: swap para um tipo específico de concreto conforme mostrado pelo OP; apenas não espere que a especialização seja usada, por exemplo, para classes derivadas desse tipo.
Dave Abrahams
15
Eu ficaria surpreso ao descobrir que as implementações ainda não usam ADL para encontrar a troca correta. Este é um assunto antigo na comissão. Se sua implementação não usa ADL para localizar a troca, envie um relatório de bug.
Howard Hinnant
3
@Sascha: Primeiro, estou definindo a função no escopo do namespace porque esse é o único tipo de definição que importa para o código genérico. Porque int et. al. não / não pode ter funções-membro, std :: sort et. al. tem que usar uma função livre; eles estabelecem o protocolo. Em segundo lugar, não sei por que você se opõe a ter duas implementações, mas a maioria das classes está condenada a ser classificada de forma ineficiente se você não aceitar ter uma troca de não membro. As regras de sobrecarga garantem que, se ambas as declarações forem vistas, a mais específica (esta) será escolhida quando a troca for chamada sem qualificação.
Dave Abrahams de
5
@ Mozza314: Depende. Um std::sortque usa ADL para trocar elementos não está em conformidade com C ++ 03, mas está em conformidade com C ++ 11. Além disso, por que -1 uma resposta baseada no fato de que os clientes podem usar código não idiomático?
JoeG
4
@curiousguy: Se ler o padrão fosse apenas uma simples questão de ler o padrão, você estaria certo :-). Infelizmente, a intenção dos autores é importante. Portanto, se a intenção original era que o ADL pudesse ou devesse ser usado, ele não foi especificado. Se não, então é apenas uma mudança de última hora para C ++ 0x, e é por isso que escrevi “na melhor das hipóteses” não especificado.
Dave Abrahams
70

Atenção Mozza314

Aqui está uma simulação dos efeitos de uma std::algorithmchamada genérica std::swape fazendo com que o usuário forneça sua troca no namespace std. Como se trata de um experimento, esta simulação usa em namespace expvez 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 mim, isso imprime:

generic exp::swap

Se o seu compilador imprimir algo diferente, ele não está implementando corretamente a "pesquisa em duas fases" para modelos.

Se o seu compilador estiver em conformidade (com qualquer um dos C ++ 98/03/11), ele fornecerá a mesma saída que mostrei. E, nesse caso, exatamente o que você teme que aconteça, realmente acontece. E colocar seu swapno namespace std( exp) não impediu que isso acontecesse.

Dave e eu somos membros do comitê e temos trabalhado nessa área do padrão por uma década (e nem sempre de acordo um com o outro). Mas essa questão foi resolvida há muito tempo, e ambos concordamos em como isso foi resolvido. Ignore a opinião / resposta de especialista de Dave nesta área por sua própria conta e risco.

Este problema veio à tona após a publicação do C ++ 98. A partir de 2001, Dave e eu começamos a trabalhar nessa área . E esta é a solução 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);
}

O resultado é:

swap(A, A)

Atualizar

Foi feita uma observação que:

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

}

trabalho! Então, por que não usar isso?

Considere o caso em que seu Aé um modelo de classe:

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

Agora não funciona novamente. :-(

Portanto, você pode swapinserir o namespace std e fazê-lo funcionar. Mas você precisa se lembrar de colocar swapno Anamespace 's para o caso quando você tem um modelo: A<T>. E uma vez que ambos os casos irá funcionar se você colocar swapem A's namespace, é apenas mais fácil de lembrar (e para ensinar aos outros) para fazê-lo apenas que uma maneira.

Howard Hinnant
fonte
4
Muito obrigado pela resposta detalhada. Eu claramente tenho menos conhecimento sobre isso e estava realmente me perguntando como a sobrecarga e a especialização poderiam produzir comportamentos diferentes. No entanto, não estou sugerindo sobrecarga, mas especialização. Quando coloco template <>seu primeiro exemplo, obtenho a saída exp::swap(A, A)do gcc. Então, por que não preferir a especialização?
voltrevo
1
Uau! Isso é realmente esclarecedor. Você definitivamente me convenceu. Acho que vou modificar um pouco sua sugestão e usar a sintaxe de amigo da classe de Dave Abrahams (ei, posso usar isso para o operador << também! :-)), a menos que você tenha um motivo para evitar isso também (além de compilar separadamente). Além disso, à luz disso, você acha que using std::swapé uma exceção à regra "nunca coloque instruções usando dentro de arquivos de cabeçalho"? Na verdade, por que não colocar using std::swapdentro <algorithm>? Suponho que isso poderia quebrar uma pequena minoria do código das pessoas. Talvez descontinuar o suporte e, eventualmente, incluí-lo?
voltrevo
3
a sintaxe do amigo na classe deve ser adequada. Eu tentaria limitar o using std::swapescopo da função dentro de seus cabeçalhos. Sim, swapé quase uma palavra-chave. Mas não, não é bem uma palavra-chave. Portanto, é melhor não exportá-lo para todos os namespaces até que seja realmente necessário. swapé muito parecido operator==. A maior diferença é que ninguém nunca pensa em chamar operator==com sintaxe de namespace qualificada (seria muito feio).
Howard Hinnant
15
@NielKirk: O que você está vendo como complicação são simplesmente muitas respostas erradas. Não há nada complicado sobre a resposta correta de Dave Abrahams: "A maneira certa de sobrecarregar a troca é gravá-la no mesmo namespace que você está trocando, para que possa ser encontrada por meio de pesquisa dependente de argumento (ADL)."
Howard Hinnant,
2
@codeshot: Desculpe. Herb tem tentado transmitir esta mensagem desde 1998: gotw.ca/publications/mill02.htm Ele não menciona a troca neste artigo. Mas esta é apenas outra aplicação do Princípio de Interface do Herb.
Howard Hinnant
53

Você não tem permissão (pelo padrão C ++) para sobrecarregar std :: swap, no entanto, você está especificamente autorizado a adicionar especializações de modelo para seus próprios tipos ao namespace std. Por exemplo

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

então, os usos nos contêineres std (e em qualquer outro lugar) escolherão sua especialização em vez da geral.

Observe também que fornecer uma implementação de classe base de swap não é bom o suficiente para seus tipos derivados. Por exemplo, se você tem

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

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

isto funcionará para classes Base, mas se você tentar trocar dois objetos Derivados ele usará a versão genérica de std porque a troca modelada é uma correspondência exata (e evita o problema de trocar apenas as partes 'base' de seus objetos derivados )

NOTA: Eu atualizei isso para remover as partes erradas da minha última resposta. D'oh! (obrigado puetzk e j_random_hacker por apontar isso)

Wilka
fonte
1
Principalmente um bom conselho, mas eu tenho que -1 por causa da sutil distinção observada por puetzk entre especializar um modelo no namespace std (que é permitido pelo padrão C ++) e sobrecarregar (que não é).
j_random_hacker
11
Votos negados porque a maneira correta de personalizar a troca é fazê-lo em seu próprio namespace (como Dave Abrahams aponta em outra resposta).
Howard Hinnant
2
Meus motivos para votar negativamente são os mesmos de Howard
Dave Abrahams,
13
@HowardHinnant, Dave Abrahams: Eu discordo. Com base em que você afirma que sua alternativa é a maneira "correta"? Como puetzk citado na norma, isso é especificamente permitido. Embora eu seja novo neste problema, realmente não gosto do método que você defende porque se eu definir Foo e trocar dessa forma, alguém que usa meu código provavelmente usará std :: swap (a, b) em vez de swap ( a, b) em Foo, que silenciosamente usa a versão padrão ineficiente.
voltrevo
5
@ Mozza314: As restrições de espaço e formatação da área de comentários não me permitiram responder completamente a você. Por favor, veja a resposta que eu adicionei intitulada "Atenção Mozza314".
Howard Hinnant
29

Embora seja correto que não se deva geralmente adicionar coisas ao std :: namespace, adicionar especializações de template para tipos definidos pelo usuário é especificamente permitido. Sobrecarregar as funções, não. Esta é uma diferença sutil :-)

17.4.3.1/1 É indefinido para um programa C ++ adicionar declarações ou definições ao namespace std ou namespaces com namespace std, a menos que especificado de outra forma. Um programa pode adicionar especializações de template para qualquer template de biblioteca padrão ao namespace std. Tal especialização (completa ou parcial) de uma biblioteca padrão resulta em um comportamento indefinido, a menos que a declaração dependa de um nome definido pelo usuário de ligação externa e a menos que a especialização de modelo atenda aos requisitos de biblioteca padrão para o modelo original.

Uma especialização de std :: swap ficaria assim:

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

Sem o bit template <>, seria uma sobrecarga, que é indefinida, ao invés de uma especialização, que é permitida. A abordagem sugerida de @Wilka para alterar o espaço de nomes padrão pode funcionar com o código do usuário (devido à pesquisa de Koenig preferir a versão sem espaço de nomes), mas não é garantido, e de fato não deveria funcionar (a implementação de STL deve usar o -qualificado std :: swap).

Há um tópico em comp.lang.c ++. Moderado com uma longa discussão sobre o assunto. A maior parte é sobre especialização parcial (o que atualmente não existe uma boa maneira de fazer).

puetzk
fonte
7
Uma razão pela qual é errado usar a especialização de template de função para isso (ou qualquer coisa): ela interage de maneira ruim com sobrecargas, das quais existem muitas para troca. Por exemplo, se você especializar o std :: swap regular para std :: vector <mytype> &, sua especialização não será escolhida em vez do swap específico do vetor padrão, porque as especializações não são consideradas durante a resolução de sobrecarga.
Dave Abrahams
4
Isso também é o que Meyers recomenda em Effective C ++ 3ed (Item 25, páginas 106-112).
jww
2
@DaveAbrahams: Se você se especializar (sem argumentos de modelo explícitas), ordenação parcial fará com que ele seja uma especialização da versão e será usado . vector
Davis Herring
1
@DavisHerring na verdade, não, quando você faz essa ordenação parcial não desempenha nenhum papel. O problema não é que você não possa telefonar; é o que acontece na presença de sobrecargas aparentemente menos específicas de swap: wandbox.org/permlink/nck8BkG0WPlRtavV
Dave Abrahams
2
@DaveAbrahams: A ordem parcial é selecionar o modelo de função para se especializar quando a especialização explícita corresponder a mais de um. A ::swapsobrecarga que você adicionou é mais especializada do que a std::swapsobrecarga para vector, portanto, ela captura a chamada e nenhuma especialização da última é relevante. Não tenho certeza de como isso é um problema prático (mas também não estou afirmando que seja uma boa ideia!).
Davis Herring