Neden C ++ 11'de üye olmayan başlangıç ​​ve bitiş işlevlerini kullanmalıyım?


197

Her standart kap, o kap için yineleyicileri döndürmek için bir beginve endyöntemine sahiptir . Bununla birlikte, C ++ 11 görünüşte denilen std::beginve ve üye işlevlerini std::endçağıran ücretsiz işlevleri tanıttı . Yani yazmak yerinebeginend

auto i = v.begin();
auto e = v.end();

sen yazardın

auto i = std::begin(v);
auto e = std::end(v);

Herb Sutter, Modern C ++ Yazma konuşmasında, bir konteyner için başlangıç ​​veya bitiş yineleyicisini istediğinizde her zaman ücretsiz işlevleri kullanmanız gerektiğini söylüyor. Ancak, neden isteyeceğinize dair ayrıntılara girmez . Koda baktığınızda, sizi bir karakterin hepsinden kurtarır. Bu nedenle, standart konteynerler gittikçe, serbest fonksiyonlar tamamen işe yaramaz gibi görünüyor. Herb Sutter, standart dışı kaplar için faydalar olduğunu belirtti, ancak yine de ayrıntılara girmedi.

Buradaki soru, std::begin ve std::endkarşılık gelen üye işlev sürümlerini çağırmanın ötesinde ne yapıyor ve neden bunları kullanmak istersiniz?


29
Bu daha az karakter, çocuklarınız için bu noktaları kaydedin: xkcd.com/297
HostileFork, SEE'ye

Bir şekilde onları kullanmaktan nefret ediyorum çünkü std::her zaman tekrarlamak zorunda kalacağım .
Michael Chourdakis

Yanıtlar:


162

Nasıl ararsın .begin()ve.end() bir C-dizi?

Serbest fonksiyonlar daha genel programlamaya izin verir, çünkü daha sonra değiştiremeyeceğiniz bir veri yapısına eklenebilirler.


7
@JonathanMDavis: şablon programlama hilelerini kullanarak endstatik olarak bildirilmiş dizilere ( int foo[5]) sahip olabilirsiniz . İşaretçiye çürütüldüğünde, elbette şansın kalmaz.
Matthieu M.

33
template<typename T, size_t N> T* end(T (&a)[N]) { return a + N; }
Hugh

6
@JonathanMDavis: Diğerleri belirtildiği gibi, elde etmek kesinlikle mümkündür beginve endzaten bir pointer kendiniz bunu çürümüş değil sürece, bir C dizisinde - @Huw durumu özetleyen. Neden yapmak istediğinize gelince: bir vektörü kullanmak için bir dizi kullanan kodu yeniden düzenlediğinizi hayal edin (ya da tam tersi, herhangi bir nedenle). Kullandığınız olduysan beginve endve belki bazı akıllı typedeffing, uygulama kodu hiç değişikliğe gerek kalmayacak (belki typedefs bazılarını hariç).
Karl Knechtel

31
@JonathanMDavis: Diziler işaretçi değil. Ve herkes için: Bu önde gelen karışıklığı sona erdirmek adına, (bazı) işaretçilerden "çürümüş diziler" olarak bahsetmeyi bırakın. Dilde böyle bir terminoloji yoktur ve bunun için bir faydası yoktur. İşaretçiler işaretçilerdir, diziler dizilerdir. Diziler dolaylı olarak ilk öğelerine bir işaretçiye dönüştürülebilir, ancak yine de diğerleriyle hiçbir ayrım olmaksızın normal bir eski işaretçidir. Tabii ki bir işaretçinin "sonunu" alamazsınız, büyük / küçük harf kapalı.
GManNickG

5
Diziler dışında, kapsayıcı benzeri yönleri açığa çıkaran çok sayıda API vardır. Açıkçası bir üçüncü taraf API'sını değiştiremezsiniz, ancak bu serbest duran başlangıç ​​/ bitiş işlevlerini kolayca yazabilirsiniz.
edA-qa mort-ora-y

35

Sınıf içeren bir kitaplığınız olduğunda durumu göz önünde bulundurun:

class SpecialArray;

2 yöntemi vardır:

int SpecialArray::arraySize();
int SpecialArray::valueAt(int);

yinelemenizi 's değerleri üzerinden bu sınıftan devralır gerekiyor ve tanımlamak begin()ve end()durumlar zaman için yöntemler

auto i = v.begin();
auto e = v.end();

Ama her zaman kullanırsan

auto i = begin(v);
auto e = end(v);

Bunu yapabilirsiniz:

template <>
SpecialArrayIterator begin(SpecialArray & arr)
{
  return SpecialArrayIterator(&arr, 0);
}

template <>
SpecialArrayIterator end(SpecialArray & arr)
{
  return SpecialArrayIterator(&arr, arr.arraySize());
}

nerede SpecialArrayIteratorgibi bir şey:

class SpecialArrayIterator
{
   SpecialArrayIterator(SpecialArray * p, int i)
    :index(i), parray(p)
   {
   }
   SpecialArrayIterator operator ++();
   SpecialArrayIterator operator --();
   SpecialArrayIterator operator ++(int);
   SpecialArrayIterator operator --(int);
   int operator *()
   {
     return parray->valueAt(index);
   }
   bool operator ==(SpecialArray &);
   // etc
private:
   SpecialArray *parray;
   int index;
   // etc
};

Şimdi ive eyasal olarak SpecialArray değerlerinin yinelenmesi ve erişimi için kullanılabilir


8
Bu template<>satırları içermemelidir . Bir şablonu özelleştirmek için değil, yeni bir işlev aşırı yükü bildiriyorsunuz.
David Stone

33

beginVe endfree işlevlerinin kullanılması, bir dolaylı katman ekler. Genellikle bu daha fazla esneklik sağlamak için yapılır.

Bu durumda birkaç kullanım düşünebilirim.

En belirgin kullanım C dizileri içindir (c işaretçileri değil).

Bir diğeri, uygun olmayan bir kapta standart bir algoritma kullanmaya çalışırken (yani kapta bir .begin()yöntem eksik ). Sadece kabı tamir edemeyeceğinizi varsayarsak, bir sonraki en iyi seçenek beginişlevi aşırı yüklemektir . Herb, her zaman beginkodunuzda tekdüzelik ve tutarlılığı teşvik etmek için işlevi kullanmanızı önerir . Hangi kapların yöntemi desteklediğini beginve hangilerinin işleve ihtiyacı olduğunu hatırlamak yerine begin.

Bir kenara, bir sonraki C ++ devri D'nin sahte üye gösterimini kopyalamalıdır . Eğer a.foo(b,c,d)bunu tanımlı değil bunun yerine çalışır foo(a,b,c,d). Sadece konudan sonra fiil düzenini tercih eden fakir insanlara yardımcı olmak için biraz sözdizimsel şeker.


5
Sözde üye notasyonu C # gibi görünüyor /. Net uzantısı yöntemleri . Tüm özellikler gibi - 'istismar' eğilimli olabilse de, çeşitli durumlar için yararlı olurlar.
Gareth Wilson

5
Sahte üye gösterimi Intellisense ile kodlama için bir nimettir; "a." ilgili fiilleri gösterir, beyin gücünü listeleri ezberlemekten kurtarır ve ilgili API işlevlerini keşfetmeye yardımcı olmak, üye olmayan işlevleri sınıflara sokmak zorunda kalmadan çoğaltma işlevini önlemeye yardımcı olabilir.
Matt Curtis

Bunu Birleşik İşlev Çağrısı Sözdizimi (UFCS) terimini kullanan C ++ içine almak için öneriler var.
underscore_d

17

Sorunuzu yanıtlamak için, varsayılan işlevler varsayılan olarak kabın üye .begin () ve .end () işlevlerini çağırmaktan başka bir şey yapmaz. itibaren<iterator> Eğer gibi standart konteynerlerin herhangi kullandığınızda otomatik olarak dahil, <vector>, <list>vb elde edersiniz:

template< class C > 
auto begin( C& c ) -> decltype(c.begin());
template< class C > 
auto begin( const C& c ) -> decltype(c.begin()); 

Sorunuzun ikinci kısmı, neden yaptıkları tek şey üye işlevlerini çağırmaksa, neden ücretsiz işlevleri tercih edeceğinizdir. Bu gerçekten ne tür bir nesneye bağlıv örnek kodunuzda . V türü standart bir kapsayıcı tipiyse, vector<T> v;o zaman serbest veya üye işlevlerini kullanmanızın önemi yoktur, aynı şeyi yaparlar. Nesneniz vaşağıdaki koddaki gibi daha genelse:

template <class T>
void foo(T& v) {
  auto i = v.begin();     
  auto e = v.end(); 
  for(; i != e; i++) { /* .. do something with i .. */ } 
}

Daha sonra üye işlevlerini kullanmak, T = C dizileri, C dizeleri, numaralandırmalar vb. İçin kodunuzu bozar. Üye olmayan işlevleri kullanarak, insanların kolayca genişletebileceği daha genel bir arabirimin reklamını yaparsınız. Serbest fonksiyon arayüzünü kullanarak:

template <class T>
void foo(T& v) {
  auto i = begin(v);     
  auto e = end(v); 
  for(; i != e; i++) { /* .. do something with i .. */ } 
}

Kod artık T = C dizileri ve C dizeleriyle çalışır. Şimdi az miktarda adaptör kodu yazıyoruz:

enum class color { RED, GREEN, BLUE };
static color colors[]  = { color::RED, color::GREEN, color::BLUE };
color* begin(const color& c) { return begin(colors); }
color* end(const color& c)   { return end(colors); }

Kodunuzu tekrarlanabilir numaralandırmalarla da uyumlu hale getirebiliriz. Herb'in asıl amacı, serbest fonksiyonları kullanmanın üye fonksiyonlarını kullanmak kadar kolay olması ve kodunuza C sekans tipleriyle geriye dönük uyumluluk ve stl olmayan sekans tipleriyle (ve gelecekteki stl tipleriyle!) Diğer geliştiricilere düşük maliyetle.


Güzel örnekler. Bununla birlikte, enumreferans olarak bir veya başka bir temel tür almam ; kopyalamak dolaylı olduğundan daha ucuz olacaktır.
underscore_d

6

Bir faydası std::beginvestd::end dış sınıflar için standart arayüz uygulamak için uzatma noktaları olarak hizmet vermeleridir.

Kullanmak isterseniz CustomContainerile sınıf beklediği döngü veya şablon işlevi için aralık tabanlı .begin()ve .end()yöntemleri, belli ki bu yöntemleri uygulamak gerekir.

Sınıf bu yöntemleri sağlarsa, bu bir sorun değildir. Bunu yapmadığında, değiştirmeniz gerekir *.

Bu, her zaman mümkün değildir, örneğin harici kütüphane, özellikle ticari ve kapalı kaynak olan.

Böyle durumlarda std::beginve std::endkullanışlı, çünkü sınıfın kendisini değiştirmeden yineleyici API'si sağlayabilir, ancak serbest fonksiyonları aşırı yükleyebilir.

Örnek:count_if bir çift yineleyici yerine bir kapsayıcı alan bir işlevi uygulamak istediğinizi varsayalım . Böyle bir kod şöyle görünebilir:

template<typename ContainerType, typename PredicateType>
std::size_t count_if(const ContainerType& container, PredicateType&& predicate)
{
    using std::begin;
    using std::end;

    return std::count_if(begin(container), end(container),
                         std::forward<PredicateType&&>(predicate));
}

Şimdi, bu gelenekle kullanmak istediğiniz herhangi bir sınıf için count_if, bu sınıfları değiştirmek yerine yalnızca iki ücretsiz işlev eklemeniz gerekir.

Şimdi, C ++, Bağımsız Değişken Bağımlı Arama adlı bir mekanizmaya sahip (ADL) sahip ve bu yaklaşımı daha da esnek hale getiriyor.

Kısacası, ADL, bir derleyici niteliksiz bir işlevi çözdüğünde (örneğin, beginyerine ad alanı olmayan işlev std::begin), bağımsız değişkenlerinin ad alanlarında bildirilen işlevleri de dikkate alacağı anlamına gelir . Örneğin:

namesapce some_lib
{
    // let's assume that CustomContainer stores elements sequentially,
    // and has data() and size() methods, but not begin() and end() methods:

    class CustomContainer
    {
        ...
    };
}

namespace some_lib
{    
    const Element* begin(const CustomContainer& c)
    {
        return c.data();
    }

    const Element* end(const CustomContainer& c)
    {
        return c.data() + c.size();
    }
}

// somewhere else:
CustomContainer c;
std::size_t n = count_if(c, somePredicate);

Bu durumda, nitelikli adların olması önemli değildir some_lib::beginve some_lib::end - çünkü içinde CustomContainerolduğu için some_lib::derleyici bu aşırı yükleri kullanacaktır.count_if .

Bu da sahip sebebi var using std::begin;ve using std::end;içinde count_if. Bu, kalifiye olmayan kullanmamıza izin verir beginve endbu nedenle ADL'ye izin verir ve derleyicinin seçmesine std::beginve std::endbaşka alternatif bulunmadığına izin verir .

Biz çerez yiyip çerez olabilir - yani özel bir uygulama sağlamak için bir yol var begin/ ' endderleyici standart olanları geri düşebilir iken.

Bazı notlar:

  • Aynı nedenden ötürü, benzer başka işlevler de vardır: std::rbegin/ rend, std::sizeve std::data.

  • Diğer cevaplardan bahsedildiği gibi, std::versiyonlar çıplak diziler için aşırı yüklenmelere sahiptir. Bu yararlıdır, ancak yukarıda tarif ettiğim şeyin özel bir örneğidir.

  • std::beginŞablon kodu yazarken ve arkadaşlarını kullanmak özellikle iyi bir fikirdir, çünkü bu, bu şablonları daha genel hale getirir. Şablon olmayanlar için de geçerliyse yöntemleri de kullanabilirsiniz.

PS: Bu yazının yaklaşık 7 yaşında olduğunun farkındayım. Karşıya çıktım çünkü yinelenen olarak işaretlenen ve burada hiçbir cevabın ADL'den bahsetmediğini keşfettiği bir soruyu cevaplamak istedim.


Herkesin yaptığı gibi hayal gücüne bırakmak yerine, ADL'yi açık bir şekilde açıklamaktan ziyade , eylemde
underscore_d

5

Üye olmayan işlevler standart kaplar için herhangi bir fayda sağlamazken, bunları kullanmak daha tutarlı ve esnek bir stil uygular. Bir zamanlar varolan bir std olmayan kap sınıfını genişletmek istiyorsanız, varolan sınıfın tanımını değiştirmek yerine, serbest işlevlerin aşırı yüklenmesini tanımlamayı tercih edersiniz. Bu nedenle std olmayan kaplar için çok kullanışlıdırlar ve her zaman serbest işlevleri kullanmak, std kapsayıcısını std olmayan bir kapla daha kolay bir şekilde değiştirebilmeniz ve altta yatan kapsayıcı türünün kodunuz için daha şeffaf olması nedeniyle kodunuzu daha esnek hale getirir. çok daha çeşitli konteyner uygulamalarını destekler.

Ama elbette bu her zaman düzgün bir şekilde tartılmalı ve aşırı soyutlama da iyi değil. Ücretsiz fonksiyonları kullanmak aşırı bir soyutlama kadar olmasa da, yine de C ++ 11 koduyla bu genç yaşta sizin için bir sorun olabilecek C ++ 03 koduyla uyumluluğu ihlal eder.


3
C ++ 03'te boost::begin()/ kullanabilirsiniz end(), bu yüzden gerçek bir uyumsuzluk yoktur :)
Marc Mutz - mmutz

1
@ MarcMutz-mmutz Pekiştirme bağımlılığı her zaman bir seçenek değildir (ve yalnızca kullanıldıklarında oldukça fazladır begin/end). Bu yüzden saf C ++ 03 ile de bir uyumsuzluk olduğunu düşünürdüm. Ancak söylendiği gibi, C ++ 11 (en azından begin/endözellikle) zaten gittikçe daha fazla benimsendiğinden, oldukça küçük (ve küçülüyor) bir uyumsuzluk .
Christian Rau

0

Sonuçta fayda, kap agnostik olacak şekilde genelleştirilmiş koddadır. std::vectorKodun kendisinde değişiklik yapmadan a , dizi veya aralıkta çalışabilir.

Ayrıca, kaplar, hatta sahip olmayan kaplar da üye olmayan menzil tabanlı erişimciler kullanılarak kod ile agnostik olarak kullanılabilecek şekilde uyarlanabilir.

Daha fazla ayrıntı için buraya bakın .

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.