C ++標準ライブラリにtransform_ifがないのはなぜですか?


84

構成コピー(1.で実行可能copy_if)を実行したいが、値のコンテナーからそれらの値へのポインターのコンテナー(2.で実行可能transform)を実行したい場合に、ユースケースが出現しました。

利用可能なツールでは、2ステップ未満でそれ行うことはできませ

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

我々は呼び出すことができますofcourseのremove_ifpv、一時の必要性を排除し、より良いまだしかし、それがために難しいことではありません実装し、このような何か(単項演算のために):

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. 利用可能なC ++標準ライブラリツールを使用したより洗練された回避策はありますか?
  2. transform_if図書館に存在しない理由はありますか?既存のツールの組み合わせは十分な回避策であり、および/またはパフォーマンスに関して適切に動作していると見なされていますか?

(IMO)この名前transform_ifは、「特定の述語が満たされた場合にのみ変換する」ことを意味します。あなたが欲しいもののより説明的な名前は次のようになりますcopy_if_and_transform
オリバーチャールズワース2014年

@OliCharlesworthは、実際にcopy_ifは「特定の述語が満たされた場合にのみコピーする」ことも意味します。同様にあいまいです。
Shahbaz 2014年

@Shahbaz:でも、そういうことcopy_ifですよね?
オリバーチャールズワース2014年

2
そのようなものの名前についての論争がそれを実装しない実際の理由であったとしても、私は驚かないでしょう!
Nikos Athanasiou 2014年

6
たぶん私はこれらのコメントに何かが欠けていますがtransform_if、変換が別の互換性のないタイプになる可能性がある場合、変換されない要素をどのようにコピーできますか?問題の実装は、まさにそのような関数に期待されることです。

回答:


33

標準ライブラリは基本アルゴリズムを優先します。

コンテナとアルゴリズムは、可能であれば互いに独立している必要があります。

同様に、既存のアルゴリズムで構成できるアルゴリズムが、省略形として含まれることはめったにありません。

ifの変換が必要な場合は、簡単に記述できます。/ today /が必要で、既製のもので構成され、オーバーヘッドが発生しない場合Boost.Rangeなどの遅延範囲を持つ範囲ライブラリを使用できます。例:

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

@hvdがコメントで指摘しているように、transform_ifdoubleは異なる型(doubleこの場合は)になります。構成の順序は重要であり、Boost Rangeを使用すると、次のように書くこともできます。

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

その結果、セマンティクスが異なります。これはポイントを家に追いやる:

それは非常に意味をなさない含まれるようにstd::filter_and_transformstd::transform_and_filterstd::filter_transform_and_filter標準ライブラリになどなど

Live OnColiruのサンプルを見る

#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
問題は、標準のアルゴリズムは怠惰ではないため、簡単に構成できないことです。
Jan Hudec 2014年

1
@JanHudec確かに。(それについては申し訳ありませんか?:))。これが、ライブラリを使用する理由です(並行性のためにAMP / TBBを使用するのと同じように、またはC#のReactive Extensionsを使用します)。多くの人々が、標準に含めるための範囲の提案と実装に取り​​組んでいます。
sehe 2014年

2
@sehe +1とても印象的で、今日は何か新しいことを学びました!Boost.RangeとPhoenixに精通していない人に、ラムダboost::phoenixなしでこのような素晴らしい述語を作成する方法を説明するドキュメント/例を見つけることができる場所を教えていただけませんか?クイックグーグル検索は、関連するものを何も返しませんでした。ありがとう!
アリ

1
「std :: filter_and_transformを含めることはほとんど意味がありません」という部分については同意しません。他のプログラミング言語も、「標準ライブラリ」でこの組み合わせを提供しています。要素のリストを一度繰り返して、変換できない要素をスキップしながら、その場で要素を変換することは完全に理にかなっています。他のアプローチでは、複数のパスが必要です。はい、BOOSTを使用できますが、実際の質問は「C ++標準ライブラリにtransform_ifがないのはなぜですか?」でした。そして私見、彼はこれを疑う権利があります。標準ライブラリにはそのような関数があるはずです。
ジョニーディー

1
@sehe「それらはすべて構成可能な抽象化を使用する」に関して:それは真実ではありません。たとえば、Rustにはまさにそのようなものがありtransform_ifます。それはと呼ばれfilter_mapます。ただし、コードを単純化するためにそこにあることを認めなければなりませんが、一方で、C ++の場合にも同じ引数を適用できます。
ジョニーディー

6

新しいforループ表記は、多くの点でコレクションのすべての要素にアクセスするアルゴリズムの必要性を減らし、ループを記述してロジックを配置するだけでよりクリーンになりました。

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

アルゴリズムを導入することで、本当に多くの価値がもたらされるのでしょうか。はい、アルゴリズムはC ++ 03に役立ち、実際に私はそれを持っていましたが、今は必要ないので、追加することに実質的な利点はありません。

実際の使用では、コードが常にそのように見えるとは限らないことに注意してください。必ずしも関数「op」と「pred」があるとは限らず、アルゴリズムに「適合」させるためにラムダを作成する必要がある場合があります。ロジックが複雑な場合は懸念を分離するのは良いことですが、入力タイプからメンバーを抽出してその値をチェックするか、コレクションに追加するだけの場合は、アルゴリズムを使用するよりもはるかに簡単です。

さらに、ある種のtransform_ifを追加したら、変換の前または後に述語を適用するか、2つの述語を使用して両方の場所に適用するかを決定する必要があります。

では、私たちは何をするつもりですか?3つのアルゴリズムを追加しますか?(そして、コンパイラーが変換の両端に述語を適用できる場合、ユーザーは間違ったアルゴリズムを簡単に選択し、コードはコンパイルされますが、間違った結果を生成する可能性があります)。

また、コレクションが大きい場合、ユーザーはイテレーターでループしますか、それともmap / reduceしますか?map / reduceを導入すると、方程式がさらに複雑になります。

基本的に、ライブラリはツールを提供し、ユーザーはここに残されて、アルゴリズムの場合のように逆ではなく、自分のやりたいことに合わせてツールを使用します。(上記のユーザーが、実際にやりたいことに合うように、accumulateを使用して物事をねじろうとした方法を参照してください)。

簡単な例として、地図。各要素について、キーが偶数の場合に値を出力します。

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

素晴らしくてシンプル。それをtransform_ifアルゴリズムに適合させたいですか?


4
上記のコードに、述語用と変換用の2つのラムダを持つtransform_ifよりもエラーの余地があると思われる場合は、説明してください。アセンブリ、CおよびC ++は異なる言語であり、場所も異なります。アルゴリズムがループよりも有利になる可能性がある唯一の場所は、「マップ/リデュース」機能であるため、大規模なコレクションに対して同時に実行できます。ただし、このようにして、ユーザーは順番にループするか、map-reduceするかを制御できます。
CashCow 2015

3
適切な機能的アプローチでは、述語とミューテーターの関数は、構成を適切に構造化する明確に定義されたブロックです。forループ本体には任意のものが含まれている可能性があり、表示されるすべてのループを注意深く分析して、その動作を理解する必要があります。
Bartek Banachewicz 2015

2
適切な関数型言語には適切な関数型アプローチを残します。これはC ++です。
CashCow 2015

3
「それをtransform_ifアルゴリズムに適合させたいですか?」これ「transform_ifアルゴリズム」ですが、すべてがハードコーディングされている点が異なります。
R.マルティーニ・フェルナンデス

2
これは、transform_ifと同等の機能を実行します。そのアルゴリズムは、コードをより複雑にするのではなく、コードを単純化するか、何らかの方法で改善することになっています。
CashCow 2015

5

久しぶりにこの質問を復活させて申し訳ありません。最近、同様の要件がありました。ブースト::オプションをとるバージョンのback_insert_iteratorを書くことでそれを解決しました:

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

このように使用されます:

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
測定されていません-ユーザーが自分の経験がCPUに縛られている(つまり決してない)と不満を言うまで、私はナノ秒よりも正確さに関心があります。しかし、私はそれが貧弱であるのを見ることができません。オプションはメモリ割り当てがなく、オプションが実際に入力されている場合にのみTsコンストラクタが呼び出されるため、非常に安価です。コンパイル時にすべてのコードパスが表示されるため、オプティマイザーがほとんどすべてのデッドコードを排除することを期待します。
リチャードホッジス2015

ええ。それが正確に汎用アルゴリズム(実際には、それらの中の一般的な構成要素)に関するものではなかった場合、私は同意します。これは、何かが単純なものでない限り、私が通常は熱狂しない場所です。さらに、オプションの処理が任意の出力イテレーターのデコレーターになることを望んでいます(したがって、アルゴリズムの構成可能性の欠如を埋めようとしている間、少なくとも出力イテレーターの構成可能性を取得します)。
sehe 2015

iteratiorのデコレータを介してオプションの挿入を処理するか、変換関数で処理するかにかかわらず、論理的に違いはありません。最終的には、フラグのテストにすぎません。最適化されたコードはどちらの方法でも同じであることがわかると思います。完全な最適化の邪魔になるのは、例外処理だけです。Tにnoexceptコンストラクターがあるとマークすると、これは解決します。
リチャードホッジス2015

transform()の呼び出しをどのような形式にしますか?構成可能なイテレータスイートを構築できると確信しています。
リチャードホッジス2015

私も:)私はあなたの提案にコメントしていました。私は他の何かを提案していませんでした(私はずっと前にそれを持っていました。代わりに範囲と構成可能なアルゴリズムを持っていきましょう:))
sehe 2015

3

この規格は、重複を最小限に抑えるように設計されています。

この特定のケースでは、単純なrange-forループを使用して、より読みやすく簡潔な方法でアルゴリズムの目的を達成できます。

// another way

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

コンパイルするように例を変更し、いくつかの診断を追加し、OPのアルゴリズムと私の両方を並べて表示しました。

#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

しばらくしてこの質問をもう一度見つけ、潜在的に有用な汎用イテレータアダプター多数考案した後、元の質問にはstd::reference_wrapper。以上のものは必要ないことに気付きました。

ポインタの代わりにそれを使用してください、そしてあなたは良いです:

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

プリント

1 1 

1

copy_if一緒に使ってもいいです。何故なの?定義OutputItコピーを参照):

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

コードを書き直します。

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
"何故なの?" -コードは人間向けだからです。私にとって、摩擦は実際にはラムダの代わりに関数オブジェクトの記述に戻るよりもひどいです。*static_cast< back_insert_iterator<vector<ha *>> &>(*this) = &arg;読めないだけでなく、不必要に具体的です。より一般的な使用法でこのc ++ 17テイクを参照してください。
sehe 2017

これは、ベースイテレータをハードコーディングしていないバージョンです(std::insert_iterator<>またはなどで使用できますstd::ostream_iterator<>)。また、変換(ラムダなど)を提供しましょう。c ++ 17、便利に見え始めた/ c ++ 11でも同じ
sehe 2017

この時点で、ベースイテレータを保持する理由はほとんどないことに注意してください。Boostにはより優れた実装が含まれていることに注意して、任意の関数を使用できますboost :: function_output_iterator。残っているのは再発明だけですfor_each_if:)
sehe 2017

実際、元の質問を読み直して、c ++ 11標準ライブラリだけを使用して理由の声を追加しましょう。
sehe 2017

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

使用法:(CONDITIONとTRANSFORMはマクロではなく、適用する条件と変換のプレースホルダーであることに注意してください)

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

この実装の本番環境を評価しますか?コピーできない要素でうまく機能しますか?または移動イテレータ?
sehe 2016年

0

これは、質問1「利用可能なC ++標準ライブラリツールでより洗練された回避策はありますか?」に対する単なる回答です。

c ++ 17を使用できる場合std::optionalは、C ++標準ライブラリ機能のみを使用したより単純なソリューションに使用できます。std::nulloptマッピングがない場合に戻るという考え方です。

Coliruでライブを見る

#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のアプローチをC ++で実装したことに注意してください。


0

std::accumulate宛先コンテナへのポインタを操作するwhichを使用できます。

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

プリント

1 1 
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.