Jak wymusić semantykę ruchu, gdy wektor rośnie?

93

Mam std::vectorobiekty określonej klasy A. Klasa jest nietrywialna i ma zdefiniowane konstruktory kopiujące i przenoszące.

std::vector<A>  myvec;

Jeśli wypełnię wektor Aobiektami (używając np. myvec.push_back(a)), Wektor będzie się powiększał, używając konstruktora kopiującego A( const A&)do tworzenia nowych kopii elementów w wektorze.

Czy mogę w jakiś sposób wymusić, że Azamiast tego używany jest konstruktor przenoszenia klasy ?

Bertwim van Beest
źródło
5
Możesz, używając implementacji wektorów obsługujących ruch.
K-ballo
2
Czy możesz bardziej szczegółowo określić, jak to osiągnąć?
Bertwim van Beest
1
Po prostu używasz implementacji wektorowej obsługującej ruch. Wygląda na to, że Twoja standardowa implementacja biblioteki (która tak przy okazji?) Nie jest świadoma ruchu. Możesz spróbować z kontenerami obsługującymi ruch z Boost.
K-ballo
1
Cóż, używam gcc 4.5.1, który jest świadomy ruchu.
Bertwim van Beest
W moim kodzie udało się uczynić konstruktor kopiujący prywatnym, mimo że konstruktor przenoszenia nie miał jawnego „noexcept”.
Arne

Odpowiedzi:

129

Musisz poinformować C ++ (konkretnie std::vector), że twój konstruktor przenoszenia i destruktor nie rzucają, używając noexcept. Następnie konstruktor przenoszenia zostanie wywołany, gdy wektor wzrośnie.

Oto jak zadeklarować i zaimplementować konstruktor ruchu, który jest przestrzegany przez std::vector:

A(A && rhs) noexcept { 
  std::cout << "i am the move constr" <<std::endl;
  ... some code doing the move ...  
  m_value=std::move(rhs.m_value) ; // etc...
}

Jeśli konstruktor nie jest noexcept, std::vectornie można go używać, ponieważ wtedy nie będzie w stanie zapewnić gwarancje wyjątków wymaganych przez normy.

Aby uzyskać więcej informacji na temat tego, co zostało powiedziane w standardzie, przeczytaj semantykę ruchu i wyjątki w języku C ++

Podziękowania dla Bo, który zasugerował, że może to mieć związek z wyjątkami. Weź również pod uwagę rady Kerreka SB i użyj ich, emplace_backjeśli to możliwe. To może być szybsze (ale często nie jest), to może być jaśniejszy i bardziej zwarta, ale jest też kilka pułapek (zwłaszcza bez wyraźnych konstruktorów).

Edytuj , często domyślnym jest to, czego chcesz: przenieś wszystko, co można przenieść, skopiuj resztę. Aby wyraźnie o to poprosić, napisz

A(A && rhs) = default;

Robiąc to, otrzymasz noexcept, jeśli to możliwe: Czy domyślny konstruktor Move jest zdefiniowany jako noexcept?

Należy pamiętać, że wczesne wersje programu Visual Studio 2015 i starsze nie obsługiwały tego, mimo że obsługuje semantykę przenoszenia.

Johan Lundberg
źródło
Z zainteresowaniem, jak ma impl „wie” czy value_type„y ruch konstruktor jest noexcept? Być może język ogranicza zestaw kandydatów wywołania funkcji, gdy zakres wywołania jest również noexceptfunkcją?
Wyścigi lekkości na orbicie
1
@LightnessRacesinOrbit Zakładam, że robi coś takiego jak en.cppreference.com/w/cpp/types/is_move_constructible . Może istnieć tylko jeden konstruktor przenoszenia, więc powinien być jasno zdefiniowany w deklaracji.
Johan Lundberg,
@LightnessRacesinOrbit, od tego czasu dowiedziałem się, że nie ma (standardowego / użytecznego) sposobu, aby naprawdę wiedzieć, czy istnieje noexceptkonstruktor ruchu. is_nothrow_move_constructiblebędzie prawdziwe, jeśli istnieje nothrowkonstruktor kopiujący. Nie znam żadnego prawdziwego przypadku drogich nothrowkonstruktorów kopiujących, więc nie jest jasne, czy to naprawdę ma znaczenie.
Johan Lundberg
Nie działa na mnie. Mój destruktor, konstruktor przenoszenia i funkcje przypisania przenoszenia są zaznaczone noexceptzarówno w nagłówku, jak i implementacji, a kiedy wykonuję push_back (std :; move), nadal wywołuje konstruktor kopiujący. Rwę sobie tutaj włosy.
AlastairG
1
@Johan Znalazłem problem. Użyłem std::move()niewłaściwego push_back()połączenia. Jeden z tych momentów, kiedy tak bardzo szukasz problemu, że nie widzisz przed sobą oczywistego błędu. A potem była pora obiadowa i zapomniałem usunąć swój komentarz.
AlastairG
17

Co ciekawe, wektor gcc 4.7.2 używa konstruktora przenoszenia tylko wtedy, gdy zarówno konstruktor przenoszenia, jak i destruktor są noexcept. Prosty przykład:

struct foo {
    foo() {}
    foo( const foo & ) noexcept { std::cout << "copy\n"; }
    foo( foo && ) noexcept { std::cout << "move\n"; }
    ~foo() noexcept {}
};

int main() {
    std::vector< foo > v;
    for ( int i = 0; i < 3; ++i ) v.emplace_back();
}

To daje oczekiwane:

move
move
move

Jednak kiedy usuwam noexceptz ~foo(), wynik jest inny:

copy
copy
copy

Myślę, że to również odpowiada na to pytanie .

Nikola Benes
źródło
Wydaje mi się, że inne odpowiedzi mówią tylko o konstruktorze ruchu, a nie o destruktorze, który nie może być wyjątkiem.
Nikola Benes
Cóż, powinno być, ale jak się okazuje, w gcc 4.7.2 tak nie było. Więc ten problem był w rzeczywistości specyficzny dla gcc. Powinien jednak zostać naprawiony w gcc 4.8.0. Zobacz powiązane pytanie dotyczące przepełnienia stosu .
Nikola Benes
-1

Wydaje się, że jedynym sposobem (dla C ++ 17 i wczesnych), aby wymusić std::vectorużycie semantyki przenoszenia przy realokacji, jest usunięcie konstruktora kopiującego :). W ten sposób użyje twoich konstruktorów przenoszenia lub umrze próbując w czasie kompilacji :).

Istnieje wiele reguł, w których std::vectorNIE WOLNO używać konstruktora przenoszenia przy ponownej alokacji, ale nic o tym, gdzie MUSI go UŻYWAĆ .

template<class T>
class move_only : public T{
public:
   move_only(){}
   move_only(const move_only&) = delete;
   move_only(move_only&&) noexcept {};
   ~move_only() noexcept {};

   using T::T;   
};

Relacja na żywo

lub

template<class T>
struct move_only{
   T value;

   template<class Arg, class ...Args, typename = std::enable_if_t<
            !std::is_same_v<move_only<T>&&, Arg >
            && !std::is_same_v<const move_only<T>&, Arg >
    >>
   move_only(Arg&& arg, Args&&... args)
      :value(std::forward<Arg>(arg), std::forward<Args>(args)...)
   {}

   move_only(){}
   move_only(const move_only&) = delete;   
   move_only(move_only&& other) noexcept : value(std::move(other.value)) {};    
   ~move_only() noexcept {};   
};

Kod na żywo

Twoja Tklasa musi mieć noexceptkonstruktor przenoszenia / operator przypisania i noexceptdestruktor. W przeciwnym razie pojawi się błąd kompilacji.

std::vector<move_only<MyClass>> vec;
wieża120
źródło
1
Nie jest konieczne usuwanie konstruktora kopiującego. Jeśli konstruktor przenoszenia jest noexcept, zostanie użyty.
balki
@balki MOŻE być używane. Standard nie WYMAGA tego teraz. Oto dyskusja groups.google.com/a/isocpp.org/forum/...
tower120