Czy mogę przenosić elementy z a std::initializer_list<T>
?
#include <initializer_list>
#include <utility>
template<typename T>
void foo(std::initializer_list<T> list)
{
for (auto it = list.begin(); it != list.end(); ++it)
{
bar(std::move(*it)); // kosher?
}
}
Ponieważ std::intializer_list<T>
wymaga specjalnej uwagi kompilatora i nie ma semantyki wartości, takiej jak zwykłe kontenery biblioteki standardowej C ++, wolę być bezpieczny niż przepraszać i pytać.
c++
templates
c++11
move-semantics
initializer-list
fredoverflow
źródło
źródło
initializer_list<T>
są ograniczone. Podobnie, odnosi się do przedmiotów. Ale myślę, że jest to wada - w zamierzeniu kompilatory mogą statycznie alokować listę w pamięci tylko do odczytu.initializer_list<int>
int
Odpowiedzi:
Nie, to nie zadziała zgodnie z przeznaczeniem; nadal będziesz otrzymywać kopie. Jestem tym dość zaskoczony, ponieważ myślałem, że
initializer_list
istnieje po to, aby przechowywać szereg tymczasowych, dopóki nie zdążąmove
.begin
iend
doinitializer_list
zwrotuconst T *
, więc wynikiemmove
w twoim kodzie jestT const &&
- niezmienne odwołanie do wartości r. Z takiego wyrażenia nie można w znaczący sposób odejść. Będzie on wiązał się z parametrem funkcji typu,T const &
ponieważ rvalues wiążą się z odwołaniami do stałej lwartości, a nadal będziesz widzieć semantykę kopiowania.Prawdopodobnie jest to spowodowane tym, że kompilator może zdecydować się na
initializer_list
utworzenie stałej zainicjowanej statycznie, ale wydaje się, że byłoby czystsze określenie jej typuinitializer_list
lubconst initializer_list
według uznania kompilatora, więc użytkownik nie wie, czy spodziewać sięconst
zmiennej, czy też wynik zbegin
iend
. Ale to tylko moje przeczucie, prawdopodobnie jest dobry powód, dla którego się mylę.Aktualizacja: Pisałem propozycję ISO dla
initializer_list
wsparcia typów move-tylko. To tylko pierwsza wersja robocza i nie została jeszcze nigdzie zaimplementowana, ale możesz ją zobaczyć, aby dokładniej przeanalizować problem.źródło
std::move
jest bezpieczne, jeśli nie produktywne. (Z wyjątkiemT const&&
konstruktorów ruchu.)const std::initializer_list<T>
czy tylkostd::initializer_list<T>
w sposób, który nie powoduje niespodzianek dość często. Weź pod uwagę, że każdy argument w elemencieinitializer_list
może być alboconst
nie, i to jest znane w kontekście wywołującego, ale kompilator musi wygenerować tylko jedną wersję kodu w kontekście wywoływanego (tzn. Wewnątrzfoo
nie ma nic o argumentach że dzwoniący przechodzi)std::initializer_list &&
przeciążenie coś zrobiło , nawet jeśli wymagane jest również przeciążenie inne niż referencyjne. Przypuszczam, że byłoby to jeszcze bardziej zagmatwane niż obecna sytuacja, która już jest zła.bar(std::move(*it)); // kosher?
Nie w taki sposób, jaki zamierzasz. Nie możesz przenieść
const
obiektu. Istd::initializer_list
zapewniaconst
dostęp tylko do jego elementów. Więc typit
jestconst T *
.Twoja próba sprawdzenia
std::move(*it)
spowoduje tylko wartość l. IE: kopia.std::initializer_list
odwołuje się do pamięci statycznej . Po to są te zajęcia. Nie możesz wyjść z pamięci statycznej, ponieważ ruch oznacza jej zmianę. Możesz tylko kopiować z niego.źródło
initializer_list
odwołuje się do stosu, jeśli jest to konieczne. (Jeśli zawartość nie jest stała, nadal jest bezpieczna dla wątków.)initializer_list
obiekt może być wartością x, ale jego zawartość (rzeczywista tablica wartości, na którą wskazuje) jestconst
, ponieważ ta zawartość może być wartościami statycznymi. Po prostu nie możesz przejść z zawartości plikuinitializer_list
.const
xwartość.move
może być bez znaczenia, ale jest legalne, a nawet możliwe, aby zadeklarować parametr, który to akceptuje. Jeśli przeniesienie określonego typu okaże się niemożliwe, może nawet działać poprawnie.std::move
. Gwarantuje to, że na podstawie inspekcji można rozpoznać, kiedy ma miejsce operacja przenoszenia, ponieważ wpływa ona zarówno na źródło, jak i miejsce docelowe (nie chcesz, aby miało to miejsce niejawnie dla nazwanych obiektów). Z tego powodu, jeśli używaszstd::move
w miejscu, w którym operacja przenoszenia nie ma miejsca (i żaden rzeczywisty ruch nie nastąpi, jeśli maszconst
xvalue), kod jest mylący. Myślę, że błędemstd::move
jest wywoływanieconst
obiektu.const &&
klasą zbieraną śmieci z zarządzanymi wskaźnikami, gdzie wszystko, co istotne, było zmienne, a przeniesienie przesunęło zarządzanie wskaźnikami, ale nie wpłynęło na zawartą wartość. Zawsze są trudne przypadki skrajne: v).To nie zadziała zgodnie z opisem, ponieważ
list.begin()
ma typconst T *
i nie ma możliwości przeniesienia się ze stałego obiektu. Projektanci języka prawdopodobnie tak zrobili, aby listy inicjalizujące zawierały na przykład stałe łańcuchowe, z których byłoby niewłaściwe przenoszenie.Jeśli jednak jesteś w sytuacji, w której wiesz, że lista inicjalizacyjna zawiera wyrażenia r-wartości (lub chcesz zmusić użytkownika do ich napisania), to istnieje sztuczka, która sprawi, że to zadziała (zainspirowała mnie odpowiedź Sumanta dla to, ale rozwiązanie jest znacznie prostsze niż tamto). Potrzebujesz, aby elementy przechowywane na liście inicjatorów nie były
T
wartościami, ale wartościami, które są hermetyzowaneT&&
. Wtedy nawet jeśli same te wartości sąconst
kwalifikowane, nadal mogą pobierać modyfikowalną wartość r.template<typename T> class rref_capture { T* ptr; public: rref_capture(T&& x) : ptr(&x) {} operator T&& () const { return std::move(*ptr); } // restitute rvalue ref };
Teraz zamiast deklarować
initializer_list<T>
argument, deklarujeszinitializer_list<rref_capture<T> >
argument. Oto konkretny przykład, obejmujący wektorstd::unique_ptr<int>
inteligentnych wskaźników, dla którego zdefiniowano tylko semantykę przenoszenia (więc same obiekty nie mogą być nigdy przechowywane na liście inicjalizacyjnej); jednak lista inicjalizatorów poniżej kompiluje się bez problemu.#include <memory> #include <initializer_list> class uptr_vec { typedef std::unique_ptr<int> uptr; // move only type std::vector<uptr> data; public: uptr_vec(uptr_vec&& v) : data(std::move(v.data)) {} uptr_vec(std::initializer_list<rref_capture<uptr> > l) : data(l.begin(),l.end()) {} uptr_vec& operator=(const uptr_vec&) = delete; int operator[] (size_t index) const { return *data[index]; } }; int main() { std::unique_ptr<int> a(new int(3)), b(new int(1)),c(new int(4)); uptr_vec v { std::move(a), std::move(b), std::move(c) }; std::cout << v[0] << "," << v[1] << "," << v[2] << std::endl; }
Jedno pytanie wymaga odpowiedzi: jeśli elementy listy inicjalizacyjnej powinny być prawdziwymi wartościami prvalues (w przykładzie są to wartości x), czy język zapewnia, że czas życia odpowiednich tymczasowych rozciąga się do momentu, w którym są używane? Szczerze mówiąc, nie sądzę, aby odpowiednia sekcja 8.5 standardu w ogóle rozwiązała ten problem. Jednak czytając 1.9: 10, mogłoby się wydawać, że odpowiednie pełne wyrażenie we wszystkich przypadkach obejmuje użycie listy inicjalizującej, więc myślę, że nie ma niebezpieczeństwa wiszących odniesień rwartości.
źródło
"Hello world"
? Jeśli odsuwasz się od nich, po prostu kopiujesz wskaźnik (lub wiążesz odniesienie).{..}
są powiązane z odwołaniami w parametrze funkcjirref_capture
. Nie przedłuża to ich życia, nadal są niszczone pod koniec pełnej ekspresji, w której zostały stworzone.std::initializer_list<rref_capture<T>>
wybraną cechę transformacji - powiedzmystd::decay_t
- aby zablokować niechciane dedukcje.Pomyślałem, że pouczające może być przedstawienie rozsądnego punktu wyjścia dla obejścia tego problemu.
Komentarze w tekście.
#include <memory> #include <vector> #include <array> #include <type_traits> #include <algorithm> #include <iterator> template<class Array> struct maker; // a maker which makes a std::vector template<class T, class A> struct maker<std::vector<T, A>> { using result_type = std::vector<T, A>; template<class...Ts> auto operator()(Ts&&...ts) const -> result_type { result_type result; result.reserve(sizeof...(Ts)); using expand = int[]; void(expand { 0, (result.push_back(std::forward<Ts>(ts)),0)... }); return result; } }; // a maker which makes std::array template<class T, std::size_t N> struct maker<std::array<T, N>> { using result_type = std::array<T, N>; template<class...Ts> auto operator()(Ts&&...ts) const { return result_type { std::forward<Ts>(ts)... }; } }; // // delegation function which selects the correct maker // template<class Array, class...Ts> auto make(Ts&&...ts) { auto m = maker<Array>(); return m(std::forward<Ts>(ts)...); } // vectors and arrays of non-copyable types using vt = std::vector<std::unique_ptr<int>>; using at = std::array<std::unique_ptr<int>,2>; int main(){ // build an array, using make<> for consistency auto a = make<at>(std::make_unique<int>(10), std::make_unique<int>(20)); // build a vector, using make<> because an initializer_list requires a copyable type auto v = make<vt>(std::make_unique<int>(10), std::make_unique<int>(20)); }
źródło
initializer_list
można przenieść z, a nie, czy ktoś miał obejścia. Poza tym głównym punktem sprzedażyinitializer_list
jest to, że jest on oparty tylko na typie elementu, a nie na liczbie elementów, a zatem nie wymaga, aby odbiorcy również byli szablonami - i to całkowicie to traci.initializer_list
ale nie podlegają wszystkim ograniczeniom, które czynią go użytecznym. :)initializer_list
(dzięki magii kompilatora) unika się konieczności tworzenia szablonów funkcji na liczbie elementów, co jest z natury wymagane przez alternatywy oparte na tablicach i / lub funkcjach wariadycznych, co ogranicza zakres przypadków, w których te ostatnie są użyteczne. W moim rozumieniu jest to dokładnie jeden z głównych powodów, dla którychinitializer_list
warto było o tym wspomnieć.Wydaje się, że nie jest to dozwolone w obecnym standardzie, jak już odpowiedziałem . Oto kolejne obejście umożliwiające osiągnięcie czegoś podobnego, definiując funkcję jako wariadyczną zamiast pobierać listę inicjalizującą.
#include <vector> #include <utility> // begin helper functions template <typename T> void add_to_vector(std::vector<T>* vec) {} template <typename T, typename... Args> void add_to_vector(std::vector<T>* vec, T&& car, Args&&... cdr) { vec->push_back(std::forward<T>(car)); add_to_vector(vec, std::forward<Args>(cdr)...); } template <typename T, typename... Args> std::vector<T> make_vector(Args&&... args) { std::vector<T> result; add_to_vector(&result, std::forward<Args>(args)...); return result; } // end helper functions struct S { S(int) {} S(S&&) {} }; void bar(S&& s) {} template <typename T, typename... Args> void foo(Args&&... args) { std::vector<T> args_vec = make_vector<T>(std::forward<Args>(args)...); for (auto& arg : args_vec) { bar(std::move(arg)); } } int main() { foo<S>(S(1), S(2), S(3)); return 0; }
Wariadyczne szablony mogą odpowiednio obsługiwać odwołania do wartości r, w przeciwieństwie do initializer_list.
W tym przykładowym kodzie użyłem zestawu małych funkcji pomocniczych do konwersji argumentów wariadycznych na wektor, aby uczynić go podobnym do oryginalnego kodu. Ale oczywiście możesz bezpośrednio napisać funkcję rekurencyjną z szablonami wariadycznymi.
źródło
initializer_list
można przenieść z, a nie, czy ktoś miał obejścia. Poza tym głównym punktem sprzedażyinitializer_list
jest to, że jest on oparty tylko na typie elementu, a nie na liczbie elementów, a zatem nie wymaga, aby odbiorcy również byli szablonami - i to całkowicie to traci.Mam znacznie prostszą implementację, która wykorzystuje klasę opakowania, która działa jak tag do oznaczania zamiaru przeniesienia elementów. Jest to koszt związany z kompilacją.
Klasa opakowująca została zaprojektowana tak, aby była używana w sposób, w jaki
std::move
jest używana, wystarczy zastąpićstd::move
jąmove_wrapper
, ale wymaga to C ++ 17. W przypadku starszych specyfikacji możesz użyć dodatkowej metody konstruktora.Będziesz musiał napisać metody / konstruktory konstruktora, które akceptują klasy opakowujące wewnątrz
initializer_list
i odpowiednio przenoszą elementy.Jeśli chcesz, aby niektóre elementy zostały skopiowane zamiast przenoszenia, utwórz kopię przed przekazaniem jej do
initializer_list
.Kod powinien zostać samodzielnie udokumentowany.
#include <iostream> #include <vector> #include <initializer_list> using namespace std; template <typename T> struct move_wrapper { T && t; move_wrapper(T && t) : t(move(t)) { // since it's just a wrapper for rvalues } explicit move_wrapper(T & t) : t(move(t)) { // acts as std::move } }; struct Foo { int x; Foo(int x) : x(x) { cout << "Foo(" << x << ")\n"; } Foo(Foo const & other) : x(other.x) { cout << "copy Foo(" << x << ")\n"; } Foo(Foo && other) : x(other.x) { cout << "move Foo(" << x << ")\n"; } }; template <typename T> struct Vec { vector<T> v; Vec(initializer_list<T> il) : v(il) { } Vec(initializer_list<move_wrapper<T>> il) { v.reserve(il.size()); for (move_wrapper<T> const & w : il) { v.emplace_back(move(w.t)); } } }; int main() { Foo x{1}; // Foo(1) Foo y{2}; // Foo(2) Vec<Foo> v{Foo{3}, move_wrapper(x), Foo{y}}; // I want y to be copied // Foo(3) // copy Foo(2) // move Foo(3) // move Foo(1) // move Foo(2) }
źródło
Zamiast używać a
std::initializer_list<T>
, możesz zadeklarować swój argument jako odwołanie do tablicy r-wartości:template <typename T> void bar(T &&value); template <typename T, size_t N> void foo(T (&&list)[N] ) { std::for_each(std::make_move_iterator(std::begin(list)), std::make_move_iterator(std::end(list)), &bar); } void baz() { foo({std::make_unique<int>(0), std::make_unique<int>(1)}); }
Zobacz przykład używając
std::unique_ptr<int>
: https://gcc.godbolt.org/z/2uNxv6źródło
Rozważ
in<T>
idiom opisany w cpptruths . Chodzi o to, aby określić lvalue / rvalue w czasie wykonywania, a następnie wywołać funkcję move lub copy-construction.in<T>
wykryje rvalue / lvalue, mimo że standardowy interfejs dostarczony przez initializer_list jest odniesieniem do const.źródło