publiczny element członkowski wymiany znajomych

169

W pięknej odpowiedzi na idiom `` kopiuj i zamień '' jest fragment kodu, potrzebuję trochę pomocy:

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

i dodaje notatkę

Istnieją inne twierdzenia, że ​​powinniśmy specjalizować się std :: swap dla naszego typu, zapewnić wymianę w klasie obok wymiany wolnej funkcji itp. Ale to wszystko jest niepotrzebne: każde właściwe użycie swap będzie poprzez niekwalifikowane wywołanie , a nasza funkcja zostanie znaleziona przez ADL. Jedna funkcja wystarczy.

Ze friendjestem trochę na „nieprzyjaznych” warunki, muszę przyznać. Tak więc moje główne pytania to:

  • wygląda jak funkcja wolna , ale znajduje się w treści klasy?
  • dlaczego to nie jest swapstatyczne ? Oczywiście nie używa żadnych zmiennych składowych.
  • "Jakiekolwiek właściwe użycie zamiany spowoduje, że zamiana przez ADL" ? ADL przeszuka przestrzenie nazw, prawda? Ale czy wygląda to również w klasach? Czy jest tutaj, gdzie friendwchodzi?

Dodatkowe pytania:

  • Czy w przypadku C ++ 11 powinienem oznaczyć moje swaps noexcept?
  • Czy w przypadku C ++ 11 i jego range-for powinienem umieścić wewnątrz klasy friend iter begin()i w friend iter end()ten sam sposób? Myślę, że friendnie jest tu potrzebne, prawda?
towi
źródło
Biorąc pod uwagę pytanie poboczne o oparte na zakresie dla: lepszym pomysłem jest pisanie funkcji składowych i pozostawienie dostępu do zakresu na begin () i end () w standardowej przestrzeni nazw (§ 24.6.5), w oparciu o zakres do użytku wewnętrznego tych z globalnych lub std przestrzeń nazw (patrz §6.5.4). Jednak ma wadę, że te funkcje są częścią nagłówka <iterator>, jeśli go nie dołączasz, możesz chcieć napisać je samodzielnie.
Vitus
2
dlaczego nie jest statyczna - ponieważ friendfunkcja w ogóle nie jest funkcją składową.
aschepler

Odpowiedzi:

175

Istnieje kilka sposobów pisania swap, niektóre lepsze niż inne. Z czasem jednak okazało się, że jedna definicja działa najlepiej. Zastanówmy się, jak moglibyśmy pomyśleć o napisaniu swapfunkcji.


Najpierw widzimy, że kontenery std::vector<>mają jednoargumentową funkcję składową swap, taką jak:

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

Oczywiście nasza klasa też powinna, prawda? No nie bardzo. Biblioteka standardowa zawiera różne niepotrzebne rzeczy , a swapjednym z nich jest członek . Czemu? Chodźmy dalej.


To, co powinniśmy zrobić, to określić, co jest kanoniczne, a jaka nasza klasa musi zrobić, aby z tym pracować. Kanoniczna metoda zamiany to std::swap. Dlatego funkcje składowe nie są przydatne: generalnie nie są one sposobem, w jaki powinniśmy zamieniać rzeczy i nie mają wpływu na zachowanie std::swap.

Cóż więc, aby wykonać std::swappracę, powinniśmy zapewnić (i std::vector<>powinniśmy zapewnić) specjalizacjęstd::swap , prawda?

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

Cóż, to z pewnością zadziałałoby w tym przypadku, ale wiąże się to z rażącym problemem: specjalizacje funkcji nie mogą być częściowe. Oznacza to, że nie możemy specjalizować klas szablonów w tym, tylko w określonych instancjach:

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

Ta metoda działa czasami, ale nie zawsze. Musi być lepszy sposób.


Jest! Możemy użyć friendfunkcji i znaleźć ją przez ADL :

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

Kiedy chcemy coś zamienić, kojarzymy †, std::swap a następnie wykonujemy połączenie bez zastrzeżeń:

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

Co to jest friend funkcja? W tej dziedzinie panuje zamieszanie.

Zanim C ++ został ustandaryzowany, friendfunkcje wykonywały coś, co nazywa się „wstrzyknięciem nazwy znajomego”, gdzie kod zachowywał się tak, jakby funkcja została napisana w otaczającej przestrzeni nazw. Na przykład były to równoważne przed standardem:

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

// turned into, pre-standard:    

struct foo
{
    friend void bar();
};

void bar()
{
    // baz
}

Jednak kiedy wynaleziono ADL, zostało to usunięte. friendFunkcja może wówczas tylko można znaleźć przez ADL; jeśli chcesz, aby była to funkcja wolna, musiała zostać zadeklarowana jako taka ( zobacz na przykład). Ale oto! Był problem.

Jeśli po prostu użyjesz std::swap(x, y), twoje przeciążenie nigdy nie zostanie znalezione, ponieważ wyraźnie powiedziałeś „szukaj stdi nigdzie indziej”! Dlatego niektórzy sugerowali napisanie dwóch funkcji: jednej jako funkcji do znalezienia przez ADL , a drugiej do obsługi jawnejstd:: kwalifikacji.

Ale jak widzieliśmy, to nie działa we wszystkich przypadkach i kończy się brzydkim bałaganem. Zamiast tego idiomatyczna zamiana poszła inną drogą: zamiast sprawić, że zadaniem klas jest zapewnienie std::swap, zadaniem swapów jest upewnienie się, że nie używają wykwalifikowanychswap , jak powyżej. I to zwykle działa całkiem nieźle, o ile ludzie o tym wiedzą. Ale w tym tkwi problem: nieintuicyjne jest korzystanie z połączenia bez kwalifikacji!

Aby to ułatwić, niektóre biblioteki, takie jak Boost, udostępniły funkcję boost::swap, która po prostu wykonuje niekwalifikowane wywołanie swap, zstd::swap jako skojarzoną przestrzeń nazw. Pomaga to w przywróceniu zwięzłości, ale nadal jest kłopotliwe.

Zauważ, że w C ++ 11 nie ma zmiany w zachowaniu std::swap, o którym my i inni błędnie myśleliśmy. Jeśli cię to ugryzło, przeczytaj tutaj .


Krótko mówiąc: funkcja składowa jest po prostu szumem, specjalizacja jest brzydka i niepełna, ale friendfunkcja jest kompletna i działa. A kiedy zamieniasz się, użyj boost::swaplub niekwalifikowany swapz std::swappowiązanym.


† Nieformalnie nazwa jest skojarzona, jeśli będzie brana pod uwagę podczas wywołania funkcji. Aby uzyskać szczegółowe informacje, przeczytaj §3.4.2. W tym przypadku std::swapzwykle nie jest brane pod uwagę; ale możemy go skojarzyć (dodać do zbioru przeciążeń rozpatrywanych przez niekwalifikowane swap), pozwalając na jego znalezienie.

GManNickG
źródło
10
Nie zgadzam się, że funkcja członkowska to tylko szum. Funkcja członkowska pozwala na np. std::vector<std::string>().swap(someVecWithData);, Co nie jest możliwe w przypadku swapfunkcji wolnej, ponieważ oba argumenty są przekazywane przez odwołanie inne niż stała.
ildjarn
3
@ildjarn: Możesz to zrobić w dwóch wierszach. Posiadanie funkcji członkowskiej narusza zasadę DRY.
GManNickG
4
@GMan: Zasada DRY nie ma zastosowania, jeśli jedna jest implementowana w ramach drugiej. W przeciwnym razie nie można by opowiadać się za klasę z wdrożeń operator=, operator+i operator+=, ale wyraźnie te podmioty na odpowiednich klasach są akceptowane / oczekiwanych istnieć dla symetrii. Moim zdaniem to samo dotyczy elementu członkowskiego swapi obszaru nazw swap.
ildjarn
3
@GMan Myślę, że rozważa zbyt wiele funkcji. Mało znane, ale nawet function<void(A*)> f; if(!f) { }może zawieść tylko dlatego, że Adeklaruje, operator!że akceptuje frównie dobrze jak fwłasne operator!(mało prawdopodobne, ale może się zdarzyć). Gdyby function<>autor pomyślał „och, mam 'operator bool', dlaczego miałbym zaimplementować 'operator!'? To naruszyłoby DRY!”, Byłoby to fatalne w skutkach. Musisz tylko mieć operator!zaimplementowaną dla Ai Akonstruktora dla a function<...>, a wszystko się zepsuje, ponieważ obaj kandydaci będą wymagać konwersji zdefiniowanych przez użytkownika.
Johannes Schaub - litb
1
Zastanówmy się, jak moglibyśmy pomyśleć o napisaniu funkcji wymiany [składowej]. Oczywiście nasza klasa też powinna, prawda? No nie bardzo. Biblioteka standardowa zawiera różnego rodzaju niepotrzebne rzeczy , a jedną z nich jest zamiana członków. Połączony GotW opowiada się za funkcją wymiany elementów.
Xeverous
7

Ten kod jest odpowiednikiem ( prawie pod każdym względem):

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

Funkcja zaprzyjaźniona zdefiniowana w klasie to:

  • umieszczony w otaczającej przestrzeni nazw
  • automatycznie inline
  • potrafi odnosić się do statycznych członków klasy bez dalszych kwalifikacji

Dokładne zasady są w sekcji [class.friend](cytuję akapity 6 i 7 szkicu C ++ 0x):

Funkcja może być zdefiniowana w zaprzyjaźnionej deklaracji klasy wtedy i tylko wtedy, gdy klasa jest klasą nielokalną (9.8), nazwa funkcji jest niekwalifikowana, a funkcja ma zasięg przestrzeni nazw.

Taka funkcja jest niejawnie wbudowana. Funkcja zaprzyjaźniona zdefiniowana w klasie znajduje się w (leksykalnym) zakresie klasy, w której jest zdefiniowana. Funkcja zaprzyjaźniona zdefiniowana poza klasą nie jest.

Ben Voigt
źródło
2
W rzeczywistości funkcje zaprzyjaźnione nie są umieszczane w otaczającej przestrzeni nazw, w standardowym C ++. Stare zachowanie nosiło nazwę „wstrzyknięcie imienia znajomego”, ale zostało zastąpione przez ADL, zastąpione w pierwszym standardzie. Zobacz górę tego . (Jednak zachowanie jest dość podobne.)
GManNickG,
1
Niezupełnie równoważne. Kod w pytaniu sprawia, że swapjest ono widoczne tylko dla ADL. Jest członkiem otaczającej przestrzeni nazw, ale jego nazwa nie jest widoczna dla innych formularzy wyszukiwania nazw. EDYCJA: Widzę, że @GMan znów był szybszy :) @Ben zawsze tak było w ISO C ++ :)
Johannes Schaub - litb
2
@Ben: Nie, iniekcja znajomego nigdy nie istniała w standardzie, ale wcześniej była szeroko stosowana, dlatego pomysł (i obsługa kompilatora) miał tendencję do kontynuowania, ale technicznie nie istnieje. friendfunkcje są znalezione tylko przez ADL, a jeśli mają być po prostu wolnymi funkcjami z frienddostępem, muszą być zadeklarowane zarówno jako friendwewnątrz klasy, jak i jako normalna wolna deklaracja funkcji poza klasą. Na przykład w tej odpowiedzi widać tę konieczność .
GManNickG
2
@towi: Ponieważ funkcja znajomego znajduje się w zakresie przestrzeni nazw, odpowiedzi na wszystkie trzy pytania powinny stać się jasne: (1) Jest to funkcja bezpłatna, dodatkowo ma znajomy dostęp do prywatnych i chronionych członków klasy. (2) W ogóle nie jest członkiem, ani instancja, ani statyczna. (3) ADL nie przeszukuje klas, ale jest to w porządku, ponieważ funkcja zaprzyjaźniona ma zasięg przestrzeni nazw.
Ben Voigt,
1
@Ben. W specyfikacji funkcja jest składową przestrzeni nazw, a frazę „funkcja ma zasięg przestrzeni nazw” można zinterpretować jako stwierdzenie, że funkcja jest składową przestrzeni nazw (w dużej mierze zależy to od kontekstu takiej instrukcji). I dodaje nazwę do tej przestrzeni nazw, która jest widoczna tylko dla ADL (w rzeczywistości IIRC niektóre części są sprzeczne z innymi częściami specyfikacji, jeśli chodzi o to, czy jakakolwiek nazwa jest dodawana, czy nie. Jednak dodanie nazwy jest potrzebne do wykrycia niezgodnych deklaracji dodanych do tego przestrzeń nazw, więc w rzeczywistości dodawana jest niewidoczna nazwa ( patrz uwaga w 3.3.1p4).
Johannes Schaub - litb