C ++ standart kitaplığında neden transform_if yok?


84

Koşullu bir kopya yapmak istendiğinde (1. ile yapılabilir copy_if), ancak bir değerler konteynerinden bu değerlere yönelik bir işaretçi konteynerine (2. ile yapılabilir transform) bir kullanım durumu ortaya çıktı .

Mevcut araçlarla bunu iki adımdan daha kısa sürede yapamıyorum :

#include <vector>
#include <algorithm>

using namespace std;

struct ha { 
    int i;
    explicit ha(int a) : i(a) {}
};

int main() 
{
    vector<ha> v{ ha{1}, ha{7}, ha{1} }; // initial vector
    // GOAL : make a vector of pointers to elements with i < 2
    vector<ha*> ph; // target vector
    vector<ha*> pv; // temporary vector
    // 1. 
    transform(v.begin(), v.end(), back_inserter(pv), 
        [](ha &arg) { return &arg; }); 
    // 2. 
    copy_if(pv.begin(), pv.end(), back_inserter(ph),
        [](ha *parg) { return parg->i < 2;  }); // 2. 

    return 0;
}

Arayabileceğimiz Tabii remove_ifüzerinde pvve geçici bir ihtiyacını ortadan kaldırmak, daha iyisi olsa da, zor değil uygulamak böyle bir şey (tekli işlemler için):

template <
    class InputIterator, class OutputIterator, 
    class UnaryOperator, class Pred
>
OutputIterator transform_if(InputIterator first1, InputIterator last1,
                            OutputIterator result, UnaryOperator op, Pred pred)
{
    while (first1 != last1) 
    {
        if (pred(*first1)) {
            *result = op(*first1);
            ++result;
        }
        ++first1;
    }
    return result;
}

// example call 
transform_if(v.begin(), v.end(), back_inserter(ph), 
[](ha &arg) { return &arg;      }, // 1. 
[](ha &arg) { return arg.i < 2; });// 2.
  1. Mevcut C ++ standart kitaplık araçlarıyla daha zarif bir çözüm var mı?
  2. transform_ifKütüphanede bulunmamasının bir nedeni var mı? Mevcut araçların kombinasyonu yeterli bir geçici çözüm mü ve / veya performans açısından iyi davrandığı düşünülüyor mu?

(IMO) Ad transform_if, "yalnızca belirli bir yüklem karşılanırsa dönüştür" anlamına gelir. İstediğiniz şey için daha açıklayıcı bir isim olurdu copy_if_and_transform!
Oliver Charlesworth

@OliCharlesworth, aslında copy_if"yalnızca belirli bir yüklem karşılanırsa kopyala" anlamına da gelir. Aynı derecede belirsiz.
Shahbaz

@Shahbaz: Ama öyle copy_ifdeğil mi?
Oliver Charlesworth

2
Böyle bir şeyin adıyla ilgili anlaşmazlıklar onu uygulamamanın gerçek nedeni olsaydı şaşırmazdım !!
Nikos Athanasiou

6
Belki bu yorumlarda bir şey eksik olabilir, ancak transform_ifdönüşüm farklı bir uyumsuz türe dönüşebiliyorsa, dönüştürmediği bu öğeleri nasıl kopyalayabilirim? Sorudaki uygulama tam olarak böyle bir işlevin yapmasını beklediğim şeydir.

Yanıtlar:


33

Standart kütüphane, temel algoritmaları tercih eder.

Kaplar ve algoritmalar mümkünse birbirinden bağımsız olmalıdır.

Benzer şekilde, mevcut algoritmalardan oluşturulabilen algoritmalar, kısaltma olarak yalnızca nadiren dahil edilir.

Eğer bir dönüşüme ihtiyacınız varsa, önemsiz bir şekilde yazabilirsiniz. İsterseniz / bugün /, hazır materyallerin oluşturulması ve ek yüke neden olmamak için , Boost.Range gibi tembel aralıklara sahip bir aralık kitaplığı kullanabilirsiniz , örneğin:

v | filtered(arg1 % 2) | transformed(arg1 * arg1 / 7.0)

@Hvd'nin bir yorumda işaret ettiği gibi, transform_ifçift ​​sonuç farklı bir türle sonuçlanır ( doublebu durumda). Beste sıralaması önemlidir ve Boost Range ile şunları da yazabilirsiniz:

 v | transformed(arg1 * arg1 / 7.0) | filtered(arg1 < 2.0)

farklı anlambilimlere neden olur. Bu, ana noktayı getiriyor:

çok az mantıklı dahil etmek std::filter_and_transform, std::transform_and_filter, std::filter_transform_and_filterstandart kütüphaneye vs vs .

Live On Coliru örneğine bakın

#include <boost/range/algorithm.hpp>
#include <boost/range/adaptors.hpp>

using namespace boost::adaptors;

// only for succinct predicates without lambdas
#include <boost/phoenix.hpp>
using namespace boost::phoenix::arg_names;

// for demo
#include <iostream>

int main()
{
    std::vector<int> const v { 1,2,3,4,5 };

    boost::copy(
            v | filtered(arg1 % 2) | transformed(arg1 * arg1 / 7.0),
            std::ostream_iterator<double>(std::cout, "\n"));
}

29
Sorun şu ki, standart algoritmalar tembel olmadıkları için kolayca oluşturulamıyor.
Jan Hudec

1
@JanHudec Indeed. (bunun için üzgünüm? :)). Bu nedenle bir kitaplık kullanıyorsunuz (eşzamanlılık için AMP / TBB'yi veya C #'da Reaktif Uzantıları kullandığınız gibi). Pek çok kişi, standarda dahil edilmek üzere bir dizi önerme + uygulama üzerinde çalışıyor.
sehe

2
@sehe +1 Çok etkileyici, bugün yeni bir şey öğrendim! Boost.Range ve Phoenix'e aşina olmayanların boost::phoenix, lambdas olmadan bu kadar güzel yüklemler yapmak için nasıl kullanılacağını açıklayan belgeleri / örnekleri nerede bulabileceğimizi bize söyler misiniz? Hızlı bir Google araması alakalı hiçbir şey döndürmedi. Teşekkürler!
Ali

1
"Std :: filter_and_transform'un dahil edilmesi çok az mantıklı" kısmına katılmıyorum. Diğer programlama dilleri de bu kombinasyonu "standart kitaplığında" sağlar. Dönüştürülemeyenleri atlarken, öğeleri anında dönüştürerek, bir dizi öğeyi yinelemek tamamen mantıklıdır. Diğer yaklaşımlar birden fazla geçişi gerektirir. Evet, BOOST kullanabilirsiniz, ancak soru aslında "C ++ standart kitaplığında neden transform_if yok?" İdi. Ve IMHO, bunu sorgulamakta haklı. Standart kitaplıkta böyle bir işlev olmalıdır.
Jonny Dee

1
@sehe "Hepsi birleştirilebilir soyutlamalar kullanıyor": bu doğru değil. Örneğin Rust, tam olarak böyle bir transform_if. Denir filter_map. Bununla birlikte, kodu basitleştirmek için orada olduğunu kabul etmeliyim, ancak diğer yandan, aynı argümanı C ++ durumunda da uygulayabilirim.
Jonny Dee

6

Yeni for döngüsü gösterimi, birçok yönden, yalnızca bir döngü yazmak ve mantığı yerine koymak için artık daha temiz olduğu koleksiyonun her öğesine erişen algoritmalara olan ihtiyacı azaltır.

std::vector< decltype( op( begin(coll) ) > output;
for( auto const& elem : coll )
{
   if( pred( elem ) )
   {
        output.push_back( op( elem ) );
   }
}

Şimdi bir algoritma koymak gerçekten çok fazla değer sağlıyor mu? Evet olsa da, algoritma C ++ 03 için faydalı olabilirdi ve gerçekten benim için bir tane vardı, şimdi bir tane ihtiyacımız yok, bu yüzden onu eklemede gerçek bir avantaj yok.

Pratik kullanımda kodunuzun her zaman tam olarak böyle görünmeyeceğini unutmayın: "op" ve "pred" işlevlerine sahip olmanız gerekmez ve bunları algoritmalara "uydurmak" için lambdalar oluşturmanız gerekebilir. Mantık karmaşıksa endişeleri ayırmak güzel olsa da, eğer mesele sadece girdi türünden bir üye çıkarmak ve değerini kontrol etmek veya koleksiyona eklemekse, bir algoritma kullanmaktan çok daha basit.

Ek olarak, bir tür transform_if eklediğinizde, yüklemi dönüşümden önce mi sonra mı uygulayacağınıza, hatta 2 yükleminiz olup olmadığına ve her iki yere de uygulayıp uygulamayacağınıza karar vermelisiniz.

Peki ne yapacağız? 3 algoritma eklensin mi? (Ve derleyicinin koşulu dönüştürmenin her iki ucuna da uygulayabilmesi durumunda, kullanıcı yanlışlıkla yanlış algoritmayı seçebilir ve kod yine de derlenir ancak yanlış sonuçlar üretir).

Ayrıca, koleksiyonlar büyükse, kullanıcı yineleyicilerle döngü yapmak mı yoksa eşlemek / küçültmek mi istiyor? Harita / küçült işlevinin tanıtılmasıyla denklemde daha fazla karmaşıklık elde edersiniz.

Esasen, kütüphane araçları sağlar ve kullanıcı burada, algoritmalarda olduğu gibi, yapmak istediklerine uyacak şekilde bunları kullanmaya bırakılır. (Yukarıdaki kullanıcının gerçekten yapmak istediklerine uyması için biriktirme kullanarak şeyleri nasıl bükmeye çalıştığını görün).

Basit bir örnek için, bir harita. Her eleman için anahtar çift ise değeri vereceğim.

std::vector< std::string > valuesOfEvenKeys
    ( std::map< int, std::string > const& keyValues )
{
    std::vector< std::string > res;
    for( auto const& elem: keyValues )
    {
        if( elem.first % 2 == 0 )
        {
            res.push_back( elem.second );
        }
    }
    return res;
}         

Güzel ve basit. Bunu bir transform_if algoritmasına uydurmak ister misiniz?


4
Yukarıdaki kodumun hatalar için 2 lambdalı bir transform_if'den daha fazla alanı olduğunu düşünüyorsanız, biri yüklem ve diğeri dönüşüm için, o zaman lütfen açıklayın. Assembly, C ve C ++ farklı dillerdir ve farklı yerleri vardır. Algoritmanın bir döngüden daha avantajlı olabileceği tek yer, "eşleme / küçültme", böylece büyük koleksiyonlar üzerinde eşzamanlı olarak çalıştırma yeteneğidir. Ancak bu şekilde kullanıcı sıralı döngü mü yoksa harita-küçültme mi yapılacağını kontrol edebilir.
CashCow

3
Uygun bir işlevsel yaklaşımda, yüklem ve mutatör için işlevler, yapıyı düzgün bir şekilde yapılandıran iyi tanımlanmış bloklardır. For döngü gövdesi içinde rastgele şeyler içerebilir ve gördüğünüz her döngü davranışını anlamak için dikkatlice analiz edilmelidir.
Bartek Banachewicz

2
Uygun işlevsel diller için uygun işlevsel yaklaşımı bırakın. Bu C ++.
CashCow

3
"Bunu bir transform_if algoritmasına uydurmak ister misiniz?" Yani olan her şeyin kodlanmış olan hariç, bir "transform_if algoritma".
R. Martinho Fernandes

2
Bir transform_if'in eşdeğerini gerçekleştirir. Sadece bu algoritmaların kodunuzu basitleştirmesi veya bir şekilde iyileştirmesi gerekiyor, daha karmaşık hale getirmemesi gerekiyor.
CashCow

5

Bu kadar uzun bir süre sonra bu soruyu dirilttiğim için özür dilerim. Geçenlerde benzer bir ihtiyacım vardı. Bunu, destek alan bir back_insert_iterator sürümü yazarak çözdüm :: isteğe bağlı:

template<class Container>
struct optional_back_insert_iterator
: public std::iterator< std::output_iterator_tag,
void, void, void, void >
{
    explicit optional_back_insert_iterator( Container& c )
    : container(std::addressof(c))
    {}

    using value_type = typename Container::value_type;

    optional_back_insert_iterator<Container>&
    operator=( const boost::optional<value_type> opt )
    {
        if (opt) {
            container->push_back(std::move(opt.value()));
        }
        return *this;
    }

    optional_back_insert_iterator<Container>&
    operator*() {
        return *this;
    }

    optional_back_insert_iterator<Container>&
    operator++() {
        return *this;
    }

    optional_back_insert_iterator<Container>&
    operator++(int) {
        return *this;
    }

protected:
    Container* container;
};

template<class Container>
optional_back_insert_iterator<Container> optional_back_inserter(Container& container)
{
    return optional_back_insert_iterator<Container>(container);
}

bunun gibi kullanıldı:

transform(begin(s), end(s),
          optional_back_inserter(d),
          [](const auto& s) -> boost::optional<size_t> {
              if (s.length() > 1)
                  return { s.length() * 2 };
              else
                  return { boost::none };
          });

1
Ölçülmedi - kullanıcılar deneyimlerinin CPU'ya bağlı olduğundan şikayet edene kadar (yani hiçbir zaman) Nanosaniyelerden çok doğrulukla ilgileniyorum. Ancak bunun zayıf olduğunu göremiyorum. Bellek ayırma olmadığı ve Ts yapıcısı yalnızca isteğe bağlı gerçekten doldurulmuşsa çağrıldığı için isteğe bağlı öğeler çok ucuzdur. İyileştiricinin neredeyse tüm ölü kodları ortadan kaldırmasını bekliyorum çünkü tüm kod yolları derleme zamanında görülebilir.
Richard Hodges

Evet. Tam olarak genel amaçlı bir algoritma (aslında, bunların içindeki genel yapı taşı) ile ilgili olmasaydı kabul ederdim. Bu, bir şey olabildiğince basit olmadığı sürece genellikle heyecanlanmadığım yer. Ayrıca, isteğe bağlı işlemenin herhangi bir çıktı yineleyicisinde bir dekoratör olmasını çok isterim (bu nedenle en azından, algoritmaların bir araya getirilebilirliği eksikliğini kapatmaya çalışırken, çıktı yineleyicilerinin bir araya getirilebilirliğini elde ederiz).
sehe

Opsiyonel eki yinelemede bir dekoratör aracılığıyla veya dönüştürme işlevinde kullanmanız mantıksal olarak hiçbir fark yoktur. Sonuçta bu sadece bir bayrak testidir. Sanırım optimize edilmiş kodun her iki şekilde de aynı olacağını göreceksiniz. Tam optimizasyonun önünde duran tek şey, istisna yönetimi olacaktır. T'nin dışında kuruculara sahip olmadığı şeklinde işaretlemek bunu iyileştirecektir.
Richard Hodges

transform () çağrısının hangi formu almasını istersiniz? Eminim bir araya getirilebilir yineleme paketi oluşturabiliriz.
Richard Hodges

Ben de :) Öneriniz üzerine yorum yapıyordum. Başka bir şey önermiyordum (uzun zaman önce yapmıştım. Bunun yerine aralıklar ve düzenlenebilir algoritmalar alalım :))
sehe

3

Standart, çoğaltmayı en aza indirecek şekilde tasarlanmıştır.

Bu özel durumda, algoritmanın amaçlarına, basit bir aralık-için döngü ile daha okunabilir ve özlü bir şekilde ulaşabilirsiniz.

// another way

vector<ha*> newVec;
for(auto& item : v) {
    if (item.i < 2) {
        newVec.push_back(&item);
    }
}

Örneği derledim, bazı teşhisler ekledim ve hem OP'nin algoritmasını hem de madeni yan yana sundum.

#include <vector>
#include <algorithm>
#include <iostream>
#include <iterator>

using namespace std;

struct ha { 
    explicit ha(int a) : i(a) {}
    int i;   // added this to solve compile error
};

// added diagnostic helpers
ostream& operator<<(ostream& os, const ha& t) {
    os << "{ " << t.i << " }";
    return os;
}

ostream& operator<<(ostream& os, const ha* t) {
    os << "&" << *t;
    return os;
}

int main() 
{
    vector<ha> v{ ha{1}, ha{7}, ha{1} }; // initial vector
    // GOAL : make a vector of pointers to elements with i < 2
    vector<ha*> ph; // target vector
    vector<ha*> pv; // temporary vector
    // 1. 
    transform(v.begin(), v.end(), back_inserter(pv), 
        [](ha &arg) { return &arg; }); 
    // 2. 
    copy_if(pv.begin(), pv.end(), back_inserter(ph),
        [](ha *parg) { return parg->i < 2;  }); // 2. 

    // output diagnostics
    copy(begin(v), end(v), ostream_iterator<ha>(cout));
    cout << endl;
    copy(begin(ph), end(ph), ostream_iterator<ha*>(cout));
    cout << endl;


    // another way

    vector<ha*> newVec;
    for(auto& item : v) {
        if (item.i < 2) {
            newVec.push_back(&item);
        }
    }

    // diagnostics
    copy(begin(newVec), end(newVec), ostream_iterator<ha*>(cout));
    cout << endl;
    return 0;
}

3

Bir süre sonra bu soruyu tekrar bulduktan ve bir sürü potansiyel olarak yararlı jenerik yineleyici bağdaştırıcıları tasarladıktan sonra , orijinal sorunun HİÇBİR ŞEYİ gerektirdiğini fark ettim std::reference_wrapper.

İşaretçi yerine onu kullanın ve iyisiniz:

Live On Coliru

#include <algorithm>
#include <functional> // std::reference_wrapper
#include <iostream>
#include <vector>

struct ha {
    int i;
};

int main() {
    std::vector<ha> v { {1}, {7}, {1}, };

    std::vector<std::reference_wrapper<ha const> > ph; // target vector
    copy_if(v.begin(), v.end(), back_inserter(ph), [](const ha &parg) { return parg.i < 2; });

    for (ha const& el : ph)
        std::cout << el.i << " ";
}

Baskılar

1 1 

1

Birlikte kullanabilirsiniz copy_if. Neden olmasın? Tanımla OutputIt( kopyaya bakın ):

struct my_inserter: back_insert_iterator<vector<ha *>>
{
  my_inserter(vector<ha *> &dst)
    : back_insert_iterator<vector<ha *>>(back_inserter<vector<ha *>>(dst))
  {
  }
  my_inserter &operator *()
  {
    return *this;
  }
  my_inserter &operator =(ha &arg)
  {
    *static_cast< back_insert_iterator<vector<ha *>> &>(*this) = &arg;
    return *this;
  }
};

ve kodunuzu yeniden yazın:

int main() 
{
    vector<ha> v{ ha{1}, ha{7}, ha{1} }; // initial vector
    // GOAL : make a vector of pointers to elements with i < 2
    vector<ha*> ph; // target vector

    my_inserter yes(ph);
    copy_if(v.begin(), v.end(), yes,
        [](const ha &parg) { return parg.i < 2;  });

    return 0;
}

4
"Neden olmasın?" - Çünkü kod insanlar içindir. Bana göre sürtünme, lambdalar yerine işlev nesnelerini yazmaya geri dönmekten daha kötü. *static_cast< back_insert_iterator<vector<ha *>> &>(*this) = &arg;hem okunamaz hem de gereksiz yere somuttur. Daha genel kullanımlarla bu c ++ 17 çekimine bakın.
sehe

İşte bir sürüm, temel yineleyiciyi sabit kodlamaz (böylece onu std::insert_iterator<>veya std::ostream_iterator<>örneğin kullanabilirsiniz ) ve ayrıca bir dönüşüm (örneğin lambda olarak) sağlayalım. c ++ 17, Yararlı görünmeye başlıyor / c ++ 11'de aynı
sehe

Unutmayın, bu noktada, temel yineleyicileri tutmak için çok az neden vardır ve basitçe: Boost'un daha iyi bir uygulama içerdiğine dikkat ederek herhangi bir işlevi kullanabilirsiniz : boost :: function_output_iterator . Şimdi geriye kalan tek şey yeniden icat etmek for_each_if:)
sehe

Aslında, orijinal soruyu yeniden okuyarak, sadece c ++ 11 standart kitaplığı kullanarak bir sebep sesi ekleyelim .
sehe

0
template <class InputIt, class OutputIt, class BinaryOp>
OutputIt
transform_if(InputIt it, InputIt end, OutputIt oit, BinaryOp op)
{
    for(; it != end; ++it, (void) ++oit)
        op(oit, *it);
    return oit;
}

Kullanım: (CONDITION ve TRANSFORM'un makrolar olmadığını, uygulamak istediğiniz koşul ve dönüşüm için yer tutucular olduklarını unutmayın)

std::vector a{1, 2, 3, 4};
std::vector b;

return transform_if(a.begin(), a.end(), b.begin(),
    [](auto oit, auto item)             // Note the use of 'auto' to make life easier
    {
        if(CONDITION(item))             // Here's the 'if' part
            *oit++ = TRANSFORM(item);   // Here's the 'transform' part
    }
);

Bu uygulama üretimini hazır olarak değerlendirir misiniz? Kopyalanamayan öğelerle iyi çalışır mı? Veya hareket yineleyiciler?
sehe

0

Bu sadece 1. sorunun cevabıdır "Mevcut C ++ standart kitaplık araçlarıyla daha zarif bir çözüm var mı?".

C ++ 17'yi kullanabiliyorsanız std::optional, yalnızca C ++ standart kitaplık işlevini kullanarak daha basit bir çözüm için kullanabilirsiniz . Fikir, std::nullopteşleştirme olmaması durumunda geri dönmektir :

Coliru'da canlı görün

#include <iostream>
#include <optional>
#include <vector>

template <
    class InputIterator, class OutputIterator, 
    class UnaryOperator
>
OutputIterator filter_transform(InputIterator first1, InputIterator last1,
                            OutputIterator result, UnaryOperator op)
{
    while (first1 != last1) 
    {
        if (auto mapped = op(*first1)) {
            *result = std::move(mapped.value());
            ++result;
        }
        ++first1;
    }
    return result;
}

struct ha { 
    int i;
    explicit ha(int a) : i(a) {}
};

int main()
{
    std::vector<ha> v{ ha{1}, ha{7}, ha{1} }; // initial vector

    // GOAL : make a vector of pointers to elements with i < 2
    std::vector<ha*> ph; // target vector
    filter_transform(v.begin(), v.end(), back_inserter(ph), 
        [](ha &arg) { return arg.i < 2 ? std::make_optional(&arg) : std::nullopt; });

    for (auto p : ph)
        std::cout << p->i << std::endl;

    return 0;
}

Rust'un yaklaşımını burada C ++ 'da uyguladığımı unutmayın .


0

std::accumulateHedef kapsayıcıya bir işaretçide hangisinin çalıştığını kullanabilirsiniz :

Live On Coliru

#include <numeric>
#include <iostream>
#include <vector>

struct ha
{
    int i;
};

// filter and transform is here
std::vector<int> * fx(std::vector<int> *a, struct ha const & v)
{
    if (v.i < 2)
    {
        a->push_back(v.i);
    }

    return a;
}

int main()
{
    std::vector<ha> v { {1}, {7}, {1}, };

    std::vector<int> ph; // target vector

    std::accumulate(v.begin(), v.end(), &ph, fx);
    
    for (int el : ph)
    {
        std::cout << el << " ";
    }
}

Baskılar

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