initializer_list i przenoszenie semantyki

98

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ć.

fredoverflow
źródło
Język rdzenia definiuje, że obiekty, do których odwołuje się an, nieinitializer_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
Johannes Schaub - litb

Odpowiedzi:

92

Nie, to nie zadziała zgodnie z przeznaczeniem; nadal będziesz otrzymywać kopie. Jestem tym dość zaskoczony, ponieważ myślałem, że initializer_lististnieje po to, aby przechowywać szereg tymczasowych, dopóki nie zdążą move.

begini enddo initializer_listzwrotu const T *, więc wynikiem movew twoim kodzie jest T 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_listutworzenie stałej zainicjowanej statycznie, ale wydaje się, że byłoby czystsze określenie jej typu initializer_listlub const initializer_listwedług uznania kompilatora, więc użytkownik nie wie, czy spodziewać się constzmiennej, czy też wynik z begini end. Ale to tylko moje przeczucie, prawdopodobnie jest dobry powód, dla którego się mylę.

Aktualizacja: Pisałem propozycję ISO dla initializer_listwsparcia 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.

Potatoswatter
źródło
11
Jeśli nie jest to jasne, nadal oznacza to, że używanie std::movejest bezpieczne, jeśli nie produktywne. (Z wyjątkiem T const&&konstruktorów ruchu.)
Luc Danton
Nie sądzę, że można zrobić cały argument, albo const std::initializer_list<T>czy tylko std::initializer_list<T>w sposób, który nie powoduje niespodzianek dość często. Weź pod uwagę, że każdy argument w elemencie initializer_listmoże być albo constnie, 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ątrz foonie ma nic o argumentach że dzwoniący przechodzi)
David Rodríguez - dribeas
1
@David: Słuszna uwaga, ale nadal byłoby przydatne, gdyby 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.
Potatoswatter
1
@JBJansen Nie można go zhakować. Nie widzę dokładnie, co ten kod ma osiągnąć wrt initializer_list, ale jako użytkownik nie masz wymaganych uprawnień, aby się z niego przenieść. Bezpieczny kod tego nie zrobi.
Potatoswatter
1
@Potatoswatter, późny komentarz, ale jaki jest stan propozycji. Czy jest jakaś niewielka szansa, że ​​trafi do C ++ 20?
WhiZTiM
20
bar(std::move(*it));   // kosher?

Nie w taki sposób, jaki zamierzasz. Nie możesz przenieść constobiektu. I std::initializer_listzapewnia constdostęp tylko do jego elementów. Więc typ itjest const T *.

Twoja próba sprawdzenia std::move(*it)spowoduje tylko wartość l. IE: kopia.

std::initializer_listodwoł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.

Nicol Bolas
źródło
Stała xvalue jest nadal wartością x i initializer_listodwołuje się do stosu, jeśli jest to konieczne. (Jeśli zawartość nie jest stała, nadal jest bezpieczna dla wątków.)
Potatoswatter
5
@Potatoswatter: Nie możesz przenieść się ze stałego obiektu. Sam initializer_listobiekt może być wartością x, ale jego zawartość (rzeczywista tablica wartości, na którą wskazuje) jest const, ponieważ ta zawartość może być wartościami statycznymi. Po prostu nie możesz przejść z zawartości pliku initializer_list.
Nicol Bolas,
Zobacz moją odpowiedź i jej dyskusję. Przeniósł iterator z dereferencją, tworząc constxwartość. movemoż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.
Potatoswatter
1
@Potatoswatter: Standard C ++ 11 zużywa dużo języka, zapewniając, że nie tymczasowe obiekty nie są faktycznie przenoszone, chyba że używasz 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żywasz std::movew miejscu, w którym operacja przenoszenia nie ma miejsca (i żaden rzeczywisty ruch nie nastąpi, jeśli masz constxvalue), kod jest mylący. Myślę, że błędem std::movejest wywoływanie constobiektu.
Nicol Bolas,
1
Może, ale nadal będę brać mniej wyjątków od reguł dotyczących możliwości wprowadzenia w błąd kodu. W każdym razie, właśnie dlatego odpowiedziałem "nie", mimo że jest to poprawne, a wynikiem jest wartość x, nawet jeśli będzie ona wiązała się tylko jako stała lwartość. Szczerze mówiąc, miałem już krótki flirt z 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).
Potatoswatter
3

To nie zadziała zgodnie z opisem, ponieważ list.begin()ma typ const 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 Twartościami, ale wartościami, które są hermetyzowane T&&. Wtedy nawet jeśli same te wartości są constkwalifikowane, 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, deklarujesz initializer_list<rref_capture<T> >argument. Oto konkretny przykład, obejmujący wektor std::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.

Marc van Leeuwen
źródło
Stałe łańcuchowe? Lubisz "Hello world"? Jeśli odsuwasz się od nich, po prostu kopiujesz wskaźnik (lub wiążesz odniesienie).
dyp
1
„Jedno pytanie wymaga odpowiedzi” Inicjatory wewnątrz {..}są powiązane z odwołaniami w parametrze funkcji rref_capture. Nie przedłuża to ich życia, nadal są niszczone pod koniec pełnej ekspresji, w której zostały stworzone.
dyp
Zgodnie z komentarzem TC z innej odpowiedzi: Jeśli masz wiele przeciążeń konstruktora, opakuj std::initializer_list<rref_capture<T>>wybraną cechę transformacji - powiedzmy std::decay_t- aby zablokować niechciane dedukcje.
Unslander Monica
2

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));
}
Richarda Hodgesa
źródło
Pytanie brzmiało, czy initializer_listmożna przenieść z, a nie, czy ktoś miał obejścia. Poza tym głównym punktem sprzedaży initializer_listjest 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.
underscore_d
1
@underscore_d masz całkowitą rację. Uważam, że dzielenie się wiedzą związaną z tym pytaniem jest samo w sobie dobrą rzeczą. W tym przypadku może pomogło to PO, a może nie - nie odpowiedział. Jednak najczęściej PO i inne osoby chętnie przyjmują dodatkowe materiały związane z tym pytaniem.
Richard Hodges,
Jasne, może rzeczywiście pomóc czytelnikom, którzy chcą czegoś podobnego, initializer_listale nie podlegają wszystkim ograniczeniom, które czynią go użytecznym. :)
underscore_d
@underscore_d które z ograniczeń przeoczyłem?
Richard Hodges
Chodzi mi tylko o to, że 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órych initializer_listwarto było o tym wspomnieć.
underscore_d
0

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.

Hiroshi Ichikawa
źródło
Pytanie brzmiało, czy initializer_listmożna przenieść z, a nie, czy ktoś miał obejścia. Poza tym głównym punktem sprzedaży initializer_listjest 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.
underscore_d
0

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::movejest używana, wystarczy zastąpić std::movemove_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_listi 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)
}
bumfo
źródło
0

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

Jorge Bellon
źródło
-1

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.

Sumant
źródło
4
Po co, u licha, chcesz określić kategorię wartości w czasie wykonywania, skoro kompilator już ją zna?
fredoverflow
1
Przeczytaj bloga i zostaw komentarz, jeśli się nie zgadzasz lub masz lepszą alternatywę. Nawet jeśli kompilator zna kategorię wartości, initializer_list nie zachowuje jej, ponieważ ma tylko iteratory const. Musisz więc „przechwycić” kategorię wartości podczas konstruowania listy initializer_list i przepuścić ją, aby funkcja mogła z niej korzystać, jak chce.
Sumant,
5
Ta odpowiedź jest w zasadzie bezużyteczna bez podążania za linkiem, a odpowiedzi SO powinny być przydatne bez podążania za linkami.
Yakk - Adam Nevraumont
1
@Sumant [kopiowanie mojego komentarza z identycznego postu w innym miejscu] Czy ten gigantyczny bałagan faktycznie zapewnia jakiekolwiek wymierne korzyści w zakresie wydajności lub zużycia pamięci, a jeśli tak, to dostatecznie dużą ilość takich korzyści, aby odpowiednio zrównoważyć to, jak okropnie wygląda i że zajmuje około godziny, aby dowiedzieć się, co próbuje zrobić? Trochę w to wątpię.
underscore_d