initializer_list ve semantiği taşı


98

Elemanları a'nın dışına taşımama izin var std::initializer_list<T>mı?

#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?
    }
}

Yana std::intializer_list<T>özel derleyici dikkat gerektiren ve C ++ standardı kütüphanesinde normal kaplarda gibi değer anlambilim yok, ben daha çok üzgünüm daha güvenli olması ve sorardı.


Çekirdek dil, bir tarafından atıfta bulunulan nesnenin initializer_list<T>sürekli olmadığını tanımlar. Gibi, nesneleri initializer_list<int>ifade eder int. Ama bence bu bir kusur - derleyicilerin salt okunur bellekte statik olarak bir liste ayırması amaçlanıyor.
Johannes Schaub -

Yanıtlar:


92

Hayır, amaçlandığı gibi çalışmaz; yine de kopyalarını alacaksınız. Buna oldukça şaşırdım, çünkü bunun initializer_listbir dizi geçiciyi onlar olana kadar tutmak için var olduğunu düşünmüştüm move.

beginve endiçin initializer_listdönüş const T *, sonucu nedenle movekodunuzda olduğu T const &&- bir değişmez rvalue referansı. Böyle bir ifadeden anlamlı bir şekilde hareket edilemez. Bu, türündeki bir işlev parametresine T const &bağlanacaktır çünkü rvalues ​​sabit lvalue referanslarına bağlanır ve yine de kopyalama anlamlarını göreceksiniz.

Muhtemelen derleyici yapmak için seçerler böylece Bunun nedeni initializer_listbir statik olarak başlatılmış sabiti, ancak onun türünü yapmak temizleyici olurdu görünüyor initializer_listveya const initializer_listkullanıcı mu alacağınızı bilmiyor böylece, hazırlayanın takdirinde constveya değişken beginve sonucudur end. Ama bu sadece benim içgüdülerim, muhtemelen yanılmamın iyi bir nedeni vardır.

Güncelleme: Yalnızca taşıma türlerini desteklemek için bir ISO önerisi yazdım initializer_list. Bu yalnızca ilk taslak ve henüz hiçbir yerde uygulanmadı, ancak sorunun daha fazla analizi için bunu görebilirsiniz.


11
Net std::movedeğilse, üretken olmasa da kullanımın güvenli olduğu anlamına gelir . ( T const&&Hareket kurucuları engelleniyor .)
Luc Danton

Tüm tartışmayı ya const std::initializer_list<T>da sadece std::initializer_list<T>çok sık sürprizlere neden olmayacak şekilde yapabileceğinizi sanmıyorum . İçindeki her bir argümanın initializer_listya constda olamayacağını ve arayanın bağlamında bilindiğini, ancak derleyicinin aranan uç bağlamında kodun yalnızca bir sürümünü oluşturması gerektiğini düşünün (yani içindeki fooargümanlar hakkında hiçbir şey bilmiyor) arayan kişi geçiyor)
David Rodríguez - dribeas

1
@David: İyi nokta, ancak std::initializer_list &&referans olmayan bir aşırı yükleme de gerekli olsa bile, aşırı yüklemenin bir şeyler yapması faydalı olacaktır . Zaten kötü olan mevcut durumdan daha kafa karıştırıcı olacağını düşünüyorum.
Potatoswatter

1
@JBJansen Etrafta hacklenemez. Bu kodun wrt initializer_list'i başarması gerektiğini tam olarak göremiyorum, ancak kullanıcı olarak ondan hareket etmek için gerekli izinlere sahip değilsiniz. Güvenli kod bunu yapmaz.
Potatoswatter

2
@Potatoswatter, geç yorum, ancak teklifin durumu nedir. C ++ 20'ye girme ihtimali var mı?
WhiZTiM

20
bar(std::move(*it));   // kosher?

İstediğin şekilde değil. Bir constnesneyi hareket ettiremezsiniz . Ve std::initializer_listyalnızca constöğelerine erişim sağlar . Türüne Yani itDİR const T *.

Arama girişiminiz std::move(*it)yalnızca bir l değeri ile sonuçlanacaktır. IE: bir kopya.

std::initializer_listreferanslar statik bellek. Sınıf bunun için. Hareketsiz bellekten hareket edemezsiniz , çünkü hareket onu değiştirmeyi gerektirir. Sadece ondan kopyalayabilirsiniz.


Sabit xvalue hala bir xvalue'dur ve initializer_listgerekliyse yığına başvurur . (İçerik sabit değilse, hala iş parçacığı açısından güvenlidir.)
Potatoswatter

5
@Potatoswatter: Sabit bir nesneden hareket edemezsiniz. initializer_listNesnesi, bir xValue olabilir, ama içeriği (bu işaret eden değerlerin gerçek dizisi) olarak var constolan içeriği statik değerler olabileceğinden. Bir initializer_list.
Nicol Bolas

Cevabımı ve tartışmasını görün. constBaşvurulan yineleyiciyi hareket ettirerek bir x değeri oluşturdu. moveanlamsız olabilir, ancak yasaldır ve tam da bunu kabul eden bir parametre bildirmek bile mümkündür. Belirli bir türü taşımak işlem gerektirmiyorsa, düzgün çalışabilir bile.
Potatoswatter

1
@Potatoswatter: C ++ 11 standardı, kullanmadığınız sürece geçici olmayan nesnelerin gerçekten taşınmamasını sağlamak için çok fazla dil kullanır std::move. Bu, hem kaynağı hem de hedefi etkilediği için bir taşıma işleminin ne zaman gerçekleştiğini incelemeden anlayabilmenizi sağlar (adlandırılmış nesneler için dolaylı olarak olmasını istemezsiniz). Eğer kullanırsanız nedenle, std::movebir taşıma işlemi nerede bir yerde değil gerçekleşmesi (ve bir varsa hiçbir gerçek hareket olur constXvalue), sonra kod yanıltıcıdır. std::moveBir constnesnede çağrılabilir olmanın yanlış olduğunu düşünüyorum .
Nicol Bolas

1
Belki, ama yine de yanıltıcı kod olasılığı konusunda kurallara daha az istisna uygulayacağım. Her neyse, bu yüzden yasal olmasına rağmen "hayır" cevabını verdim ve sonuç yalnızca sabit bir değer olarak bağlanacak olsa bile bir x değeri. Dürüst olmak gerekirse, const &&yönetilen işaretçilerle çöp toplama sınıfında zaten kısa bir flört yaşadım , burada ilgili her şey değiştirilebilir ve hareket ediyor, işaretçi yönetimini hareket ettirdi, ancak içerilen değeri etkilemedi. Her zaman zor uç durumlar vardır: v).
Potatoswatter

3

Bu belirtildiği gibi çalışmaz, çünkü list.begin()türü const T *vardır ve sabit bir nesneden hareket etmenin bir yolu yoktur. Dil tasarımcıları muhtemelen bunu, başlatıcı listelerinin, taşınması uygun olmayan örneğin dize sabitleri içermesine izin vermek için yaptılar.

Ancak, başlatıcı listesinin rvalue ifadeleri içerdiğini bildiğiniz bir durumdaysanız (veya kullanıcıyı bunları yazmaya zorlamak istiyorsanız) o zaman işe yarayacak bir numara var (Sumant'ın cevabından ilham aldım. bu, ancak çözüm bundan çok daha basit). Başlatıcı listesinde depolanan öğelerin Tdeğerler değil , kapsayıcı değerler olması gerekir T&&. Daha sonra bu değerlerin kendileri constnitelikli olsa bile , yine de değiştirilebilir bir değer elde edebilirler.

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

Şimdi bir initializer_list<T>argüman bildirmek yerine, bir argüman ilan edersiniz initializer_list<rref_capture<T> >. Burada, std::unique_ptr<int>sadece hareket anlamının tanımlandığı akıllı işaretçilerden oluşan bir vektörü içeren somut bir örnek var (bu nedenle bu nesnelerin kendileri asla bir başlatıcı listesinde saklanamaz); ancak aşağıdaki başlatıcı listesi sorunsuz bir şekilde derlenir.

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

Bir sorunun cevaba ihtiyacı var: Eğer başlatıcı listesinin elemanları doğru prvalues ​​olması gerekiyorsa (örnekte bunlar xdeğerlerse), dil karşılık gelen geçicilerin yaşam süresinin kullanıldıkları noktaya kadar uzamasını sağlıyor mu? Açıkçası, standardın ilgili bölüm 8.5'in bu konuyu ele aldığını sanmıyorum. Bununla birlikte, 1.9: 10 okunduğunda , her durumda ilgili tam ifadenin başlatıcı listesinin kullanımını kapsadığı görülüyor , bu nedenle rvalue referanslarının sarkma tehlikesi olmadığını düşünüyorum.


Dize sabitleri? Beğen "Hello world"? Onlardan hareket ederseniz, sadece bir işaretçiyi kopyalarsınız (veya bir referansı bağlarsınız).
14'te

1
"Bir sorunun bir yanıta ihtiyacı var" İçerideki başlatıcılar {..}işlev parametresindeki referanslara bağlıdır rref_capture. Bu onların ömürlerini uzatmaz, yaratıldıkları tam ifadenin sonunda hala yok edilirler.
14'te

Başına TC başka cevaptan un yorumunu: Eğer yapıcı birden fazla yüke varsa, wrap std::initializer_list<rref_capture<T>>- söz hakkından, sizin seçtiğiniz bazı dönüşüm özelliği de std::decay_tistenmeyen kesinti engellemek için -.
Unslander Monica

2

Geçici bir çözüm için makul bir başlangıç ​​noktası önermenin öğretici olabileceğini düşündüm.

Satır içi yorumlar.

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

Soru şuydu initializer_list, herhangi birinin geçici çözümlere sahip olup olmadığı değil, bir yerden taşınabilir mi? Ayrıca, ana satış noktası initializer_list, yalnızca öğe türüne göre şablon haline getirilmiş olmasıdır, öğe sayısı değil ve bu nedenle alıcıların da şablonlanmasını gerektirmez - ve bu tamamen kaybeder.
underscore_d

1
@underscore_d kesinlikle haklısın. Soruyla ilgili bilgi paylaşımının başlı başına iyi bir şey olduğu görüşündeyim. Bu durumda, belki OP'ye yardımcı oldu ve belki de yaramadı - yanıt vermedi. Ancak çoğu kez, OP ve diğerleri soruyla ilgili ekstra materyali hoş karşılamaktadır.
Richard Hodges

Elbette, benzer bir şey isteyen initializer_listancak onu yararlı kılan tüm kısıtlamalara tabi olmayan okuyucular için gerçekten yardımcı olabilir . :)
underscore_d

@underscore_d hangi kısıtlamaları gözden kaçırdım?
Richard Hodges

Demek istediğim, initializer_list(derleyici sihri aracılığıyla), dizilere ve / veya çeşitli işlevlere dayanan alternatiflerin doğası gereği ihtiyaç duyduğu, böylece ikincisinin kullanılabilir olduğu durumların aralığını sınırlayan öğe sayısı üzerinde işlevler şablonlamaktan kaçınıyor. Benim anlayışıma göre, bu kesinlikle sahip olmanın ana gerekçelerinden biridir initializer_list, bu yüzden bahsetmeye değer görünüyordu.
underscore_d

0

Halihazırda yanıtlandığı gibi mevcut standartta buna izin verilmiyor gibi görünüyor . Bir başlatıcı listesi almak yerine işlevi değişken olarak tanımlayarak benzer bir şey elde etmek için başka bir geçici çözüm var.

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

Variadic şablonlar, initializer_list'in aksine r-değeri referanslarını uygun şekilde işleyebilir.

Bu örnek kodda, değişken bağımsız değişkenleri bir vektöre dönüştürmek ve orijinal koda benzer hale getirmek için bir dizi küçük yardımcı işlev kullandım. Ama tabii ki bunun yerine doğrudan değişken şablonlarla özyinelemeli bir işlev yazabilirsiniz.


Soru şuydu initializer_list, herhangi birinin geçici çözümlere sahip olup olmadığı değil, bir yerden taşınabilir mi? Ayrıca, ana satış noktası initializer_list, yalnızca öğe türüne göre şablon haline getirilmiş olmasıdır, öğe sayısı değil ve bu nedenle alıcıların da şablonlanmasını gerektirmez - ve bu tamamen kaybeder.
underscore_d

0

Öğeleri taşıma amacını işaretlemek için etiket görevi gören bir sarmalayıcı sınıfından yararlanan çok daha basit bir uygulamaya sahibim. Bu bir derleme zamanı maliyetidir.

Sargı sınıf bir şekilde kullanılmak üzere tasarlanmıştır std::movesadece yerine kullanılır std::moveile move_wrapper, ancak bu C ++ 17 gerektirir. Daha eski özellikler için ek bir oluşturucu yöntemi kullanabilirsiniz.

İçerideki sarmalayıcı sınıflarını kabul initializer_listeden ve öğeleri buna göre hareket ettiren oluşturucu yöntemler / yapıcılar yazmanız gerekir .

Taşınmak yerine kopyalanacak bazı öğelere ihtiyacınız varsa, onu aktarmadan önce bir kopya oluşturun initializer_list.

Kod kendi kendine belgelenmelidir.

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

0

A kullanmak yerine, std::initializer_list<T>bağımsız değişkeninizi bir dizi rvalue başvurusu olarak bildirebilirsiniz:

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

Şu kullanarak örneğe bakın std::unique_ptr<int>: https://gcc.godbolt.org/z/2uNxv6


-1

Cpptruths'dain<T> açıklanan deyimi düşünün . Buradaki fikir, çalışma zamanında lvalue / rvalue'yu belirlemek ve ardından taşıma veya kopyalama-oluşturma çağrısı yapmaktır. initializer_list tarafından sağlanan standart arayüz sabit referans olmasına rağmen rvalue / lvalue'yu algılar.in<T>


4
Derleyici zaten bildiği halde neden çalışma zamanında değer kategorisini belirlemek istersiniz?
fredoverflow

1
Lütfen blogu okuyun ve katılmıyorsanız veya daha iyi bir alternatifiniz varsa bana bir yorum bırakın. Derleyici değer kategorisini bilse bile, initializer_list onu korumaz çünkü sadece const yineleyicileri vardır. Bu nedenle, initializer_list'i oluşturduğunuzda ve bunu ilettiğinizde değer kategorisini "yakalamanız" gerekir, böylece işlev onu istediği gibi kullanabilir.
Sumant

5
Bu cevap, bağlantıyı takip etmeden temelde faydasızdır ve SO cevapları, bağlantıları takip etmeden faydalı olmalıdır.
Yakk - Adam Nevraumont

1
@Sumant [yorumumu başka bir yerdeki benzer bir gönderiden kopyalamak] Bu muazzam karmaşa, performans veya bellek kullanımına gerçekten ölçülebilir faydalar sağlıyor mu ve eğer öyleyse, ne kadar korkunç göründüğünü ve gerçeğini yeterince dengelemek için yeterince büyük miktarda bu tür faydalar sağlıyor mu? Ne yapmaya çalıştığını anlamak yaklaşık bir saat mi sürüyor? Bundan şüpheliyim.
underscore_d
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.