Kiedy uczynić typ nieruchomym w C ++ 11?

127

Byłem zaskoczony, że to nie pojawiło się w moich wynikach wyszukiwania, pomyślałem, że ktoś by zapytał o to wcześniej, biorąc pod uwagę użyteczność semantyki ruchu w C ++ 11:

Kiedy muszę (lub czy jest to dobry pomysł) uczynić klasę nieruchomą w C ++ 11?

( To znaczy przyczyny inne niż problemy ze zgodnością z istniejącym kodem).

user541686
źródło
2
boost jest zawsze o krok do przodu - „typy drogie w przenoszeniu” ( boost.org/doc/libs/1_48_0/doc/html/container/move_emplace.html )
SChepurin
1
Myślę, że jest to bardzo dobre i przydatne pytanie ( +1ode mnie) z bardzo dokładną odpowiedzią od Herba (lub jego bliźniaka, jak się wydaje ), więc zrobiłem to wpis w FAQ. Jeśli ktoś sprzeciwi się, po prostu pinguje mnie w salonie , więc można to tam omówić.
sbi
1
Ruchome klasy AFAIK mogą nadal podlegać wycinaniu, dlatego sensowne jest zakazanie przenoszenia (i kopiowania) wszystkich polimorficznych klas bazowych (tj. Wszystkich klas bazowych z funkcjami wirtualnymi).
Filip
1
@Mehrdad: Mówię tylko, że „T ma konstruktor ruchu” i „ T x = std::move(anotherT);bycie legalnym” nie są równoważne. To ostatnie jest żądaniem ruchu, które może spaść z powrotem na kopiującego w przypadku, gdy T nie ma żadnego z nich. Więc co dokładnie oznacza „ruchomy”?
sellibitze
1
@Mehrdad: Sprawdź sekcję biblioteki standardowej C ++, aby dowiedzieć się, co oznacza „MoveConstructible”. Niektóre iteratory mogą nie mieć konstruktora przenoszenia, ale nadal jest to MoveConstructible. Zwróć uwagę na różne definicje osób „ruchomych”, jakie mają na myśli.
sellibitze

Odpowiedzi:

110

Odpowiedź Herba (zanim została zredagowana) faktycznie podała dobry przykład typu, którego nie można przenosić:std::mutex .

Natywny typ muteksu systemu operacyjnego (np. pthread_mutex_tNa platformach POSIX) może nie być „niezmienny względem lokalizacji”, co oznacza, że ​​adres obiektu jest częścią jego wartości. Na przykład system operacyjny może przechowywać listę wskaźników do wszystkich zainicjowanych obiektów mutex. Jeśli std::mutexzawiera natywny typ muteksu systemu operacyjnego jako element członkowski danych, a adres typu natywnego musi pozostać niezmieniony (ponieważ system operacyjny utrzymuje listę wskaźników do swoich muteksów), wówczas każdy z nich std::mutexmusiałby przechowywać natywny typ muteksu na stercie, aby pozostał na w tej samej lokalizacji, gdy są przenoszone między std::mutexobiektami lub std::mutexnie mogą się poruszać. Przechowywanie go na stercie nie jest możliwe, ponieważ std::mutexma constexprkonstruktora i musi kwalifikować się do stałej inicjalizacji (tj. Inicjalizacji statycznej), aby globalnastd::mutex jest gwarantowane, że zostanie utworzony przed rozpoczęciem wykonywania programu, więc jego konstruktor nie może go użyć new.std::mutex

To samo rozumowanie dotyczy innych typów, które zawierają coś, co wymaga stałego adresu. Jeśli adres zasobu musi pozostać niezmieniony, nie przenoś go!

Jest jeszcze jeden argument przemawiający za tym, aby się nie ruszać, std::mutexa mianowicie, że byłoby bardzo trudno zrobić to bezpiecznie, ponieważ musiałbyś wiedzieć, że nikt nie próbuje zablokować muteksu w momencie, gdy jest przenoszony. Ponieważ muteksy są jednym z elementów budulcowych, których możesz użyć do zapobiegania wyścigom danych, byłoby niefortunne, gdyby same nie były bezpieczne przed rasami! Z nieruchomością std::mutexwiesz, że jedyną rzeczą, jaką każdy może z nią zrobić po jej zbudowaniu i zanim zostanie zniszczona, jest zablokowanie jej i odblokowanie, a operacje te są wyraźnie gwarantowane, że są bezpieczne dla wątków i nie wprowadzają wyścigów danych. Ten sam argument odnosi się do std::atomic<T>obiektów: jeśli nie można ich przesunąć atomowo, nie byłoby możliwe ich bezpieczne przeniesienie, inny wątek może próbować wywołaćcompare_exchange_strongna obiekcie w momencie, gdy jest przenoszony. Więc innym przypadkiem, w którym typy nie powinny być ruchome, jest sytuacja, w której są one niskopoziomowymi blokami konstrukcyjnymi bezpiecznego kodu współbieżnego i muszą zapewniać atomowość wszystkich operacji na nich. Jeśli wartość obiektu może zostać przeniesiona do nowego obiektu w dowolnym momencie, musisz użyć zmiennej atomowej, aby chronić każdą zmienną atomową, aby wiedzieć, czy można jej bezpiecznie używać, czy została przeniesiona ... i zmienną atomową do ochrony ta zmienna atomowa i tak dalej ...

Myślę, że uogólniłbym stwierdzenie, że kiedy obiekt jest po prostu czystą pamięcią, a nie typem, który działa jako posiadacz wartości lub abstrakcja wartości, nie ma sensu go przenosić. Podstawowe typy, takie jak intnie można się ruszać: przenoszenie ich to tylko kopia. Nie możesz wyrwać wnętrzności z an int, możesz skopiować jego wartość, a następnie ustawić ją na zero, ale nadal jest to intz wartością, to tylko bajty pamięci. Ale intjest nadal ruchomyw terminach językowych, ponieważ kopia jest prawidłową operacją przenoszenia. Jednak w przypadku typów nie do skopiowania, jeśli nie chcesz lub nie możesz przenieść fragmentu pamięci, a także nie możesz skopiować jego wartości, oznacza to, że nie można go przenieść. Muteks lub zmienna atomowa to określone miejsce w pamięci (traktowane specjalnymi właściwościami), więc nie ma sensu przenosić, a także nie można ich kopiować, więc nie można ich przenosić.

Jonathan Wakely
źródło
17
+1 mniej egzotyczny przykład czegoś, czego nie można przenieść, ponieważ ma specjalny adres, jest węzłem w ukierunkowanej strukturze grafu.
Potatoswatter
3
Jeśli mutex nie nadaje się do kopiowania i nie można go przenosić, jak mogę skopiować lub przenieść obiekt, który zawiera muteks? (Jak klasa bezpieczna
wątkowo
4
@ tr3w, nie możesz, chyba że utworzysz muteks na stercie i utrzymasz go za pomocą unique_ptr lub podobnego
Jonathan Wakely,
2
@ tr3w: Czy nie mógłbyś po prostu przenieść całej klasy z wyjątkiem części mutex?
user541686
3
@BenVoigt, ale nowy obiekt będzie miał własny mutex. Myślę, że ma na myśli posiadanie operacji przenoszenia zdefiniowanych przez użytkownika, które przenoszą wszystkich członków oprócz elementu mutex. A co z tego, że stary obiekt wygasa? Jego mutex wygasa wraz z nim.
Jonathan Wakely,
57

Krótka odpowiedź: jeśli typ można skopiować, powinien być również przenośny. Jednak sytuacja odwrotna nie jest prawdą: niektóre typy, takie jak, std::unique_ptrsą ruchome, ale kopiowanie ich nie ma sensu; są to oczywiście typy tylko do przenoszenia.

Nieco dłuższa odpowiedź następuje ...

Istnieją dwa główne rodzaje typów (między innymi te bardziej specjalnego przeznaczenia, takie jak cechy):

  1. Typy wartościowe, takie jak intlub vector<widget>. Reprezentują one wartości i naturalnie powinny być kopiowalne. W C ++ 11 generalnie powinieneś myśleć o przenoszeniu jako o optymalizacji kopiowania, więc wszystkie typy do kopiowania powinny być naturalnie przenośne ... przenoszenie jest po prostu skutecznym sposobem wykonywania kopii w często powszechnym przypadku, którego nie robisz ' nie potrzebuję już oryginalnego obiektu i i tak go zniszczę.

  2. Typy podobne do odwołań, które istnieją w hierarchiach dziedziczenia, takich jak klasy podstawowe i klasy z wirtualnymi lub chronionymi funkcjami składowymi. Są one zwykle utrzymywane przez wskaźnik lub odwołanie, często base*lub base&, więc nie zapewniają konstrukcji kopiowania, aby uniknąć cięcia; jeśli chcesz uzyskać inny obiekt, taki jak istniejący, zwykle wywołujesz funkcję wirtualną, taką jak clone. Nie wymagają one konstrukcji ani przypisania przesunięcia z dwóch powodów: Nie można ich kopiować i mają już jeszcze bardziej wydajną naturalną operację „przenoszenia” - po prostu kopiujesz / przesuwasz wskaźnik do obiektu, a sam obiekt nie w ogóle trzeba przenieść do nowej lokalizacji pamięci.

Większość typów należy do jednej z tych dwóch kategorii, ale są też inne typy, które są również przydatne, tylko rzadziej. W szczególności tutaj typy, które wyrażają unikalną własność zasobu, takie jak std::unique_ptr, są naturalnie typami tylko do przenoszenia, ponieważ nie są wartościowe (nie ma sensu ich kopiować), ale używasz ich bezpośrednio (nie zawsze za pomocą wskaźnika lub odniesienia) i dlatego chcą przenosić obiekty tego typu z jednego miejsca do drugiego.

Herb Sutter
źródło
61
Czy prawdziwy Herb Sutter mógłby wstać? :)
fredoverflow
6
Tak, przerzuciłem się z używania jednego konta Google OAuth na inne i nie mogę zawracać sobie głowy szukaniem sposobu na połączenie dwóch danych logowania, które mi tutaj dają. (Kolejny argument przeciwko OAuth, jeden z bardziej przekonujących). Prawdopodobnie nie użyję drugiego ponownie, więc na razie tego będę używał w sporadycznych wpisach SO.
Herb Sutter
7
Myślałem, że to std::mutexjest nieruchome, ponieważ muteksy POSIX są używane przez adres.
Puppy
9
@SChepurin: Właściwie to nazywa się HerbOverflow.
sbi
26
To zbiera dużo głosów pozytywnych, czy nikt nie zauważył, że mówi, kiedy typ powinien być tylko do ruchu, a nie jest to pytanie? :)
Jonathan Wakely
18

Właściwie, kiedy rozglądam się po okolicy, zauważyłem, że wiele typów w C ++ 11 nie jest ruchomych:

  • wszystkie mutextypy ( recursive_mutex, timed_mutex, recursive_timed_mutex,
  • condition_variable
  • type_info
  • error_category
  • locale::facet
  • random_device
  • seed_seq
  • ios_base
  • basic_istream<charT,traits>::sentry
  • basic_ostream<charT,traits>::sentry
  • wszystkie atomictypy
  • once_flag

Najwyraźniej jest dyskusja na temat Clang: https://groups.google.com/forum/?fromgroups=#!topic/comp.std.c++/pCO1Qqb3Xa4

billz
źródło
1
... iteratory nie powinny być ruchome ?! Co dlaczego?
user541686
tak, myślę, że iterators / iterator adaptorspowinno być edytowane, ponieważ C ++ 11 ma move_iterator?
billz
Ok, teraz jestem po prostu zdezorientowany. Czy mówisz o iteratorach, które przenoszą swoje cele , czy o przenoszeniu samych iteratorów ?
user541686
1
Tak jest std::reference_wrapper. Ok, inne rzeczywiście wydają się być nieruchome.
Christian Rau,
1
Te wydają się podzielić na trzy kategorie: typy związane współbieżności-1. niskopoziomowe (Atomics, muteksy), 2. polimorficzne klas bazowych ( ios_base, type_info, facet), 3. Różne dziwne rzeczy ( sentry). Prawdopodobnie jedyne nieruchome klasy, które napisze przeciętny programista, należą do drugiej kategorii.
Philipp
0

Kolejny powód, który znalazłem - wydajność. Powiedzmy, że masz klasę „a”, która ma wartość. Chcesz wyświetlić interfejs, który pozwala użytkownikowi zmienić wartość przez ograniczony czas (dla zakresu).

Sposobem na osiągnięcie tego jest zwrócenie obiektu `` scope guard '' z `` a '', który ustawia wartość z powrotem w jego destruktorze, na przykład:

class a 
{ 
    int value = 0;

  public:

    struct change_value_guard 
    { 
        friend a;
      private:
        change_value_guard(a& owner, int value) 
            : owner{ owner } 
        { 
            owner.value = value;
        }
        change_value_guard(change_value_guard&&) = delete;
        change_value_guard(const change_value_guard&) = delete;
      public:
        ~change_value_guard()
        {
            owner.value = 0;
        }
      private:
        a& owner;
    };

    change_value_guard changeValue(int newValue)
    { 
        return{ *this, newValue };
    }
};

int main()
{
    a a;
    {
        auto guard = a.changeValue(2);
    }
}

Gdybym sprawił, że change_value_guard był ruchomy, musiałbym dodać „if” do jego destruktora, który sprawdzałby, czy osłona została przeniesiona - to dodatkowe „jeśli” i wpływ na wydajność.

Tak, jasne, prawdopodobnie można to zoptymalizować za pomocą dowolnego rozsądnego optymalizatora, ale nadal fajnie, że język (wymaga to jednak C ++ 17, aby móc zwrócić nieruchomy typ, wymaga gwarantowanej elizji kopiowania) nie wymaga nas zapłacić, jeśli i tak nie zamierzamy przesunąć strażnika, poza zwróceniem go z funkcji tworzącej (zasada nie-płacić za to, czego nie używasz).

saarraz1
źródło