簡體   English   中英

為什么C++標准庫中沒有transform_if?

[英]Why is there no transform_if in the C++ standard library?

想要做一個contitional拷貝(1.可行與當用例出現copy_if ),但是從值容器的指針的一個容器的那些值(2.可行與transform )。

可用的工具,我不能做到這一點,在不到兩個步驟:

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

當然,我們可以在pv上調用remove_if並消除對臨時的需要,但更好的是,實現(對於一元運算)這樣的事情並不難:

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是否有原因? 現有工具的組合是否足以解決問題和/或被認為表現良好?

標准庫支持基本算法。

如果可能,容器和算法應該相互獨立。

同樣,可以由現有算法組成的算法很少被包括在內,作為速記。

如果您需要轉換 if,您可以輕松編寫它。 如果你想要它 /today/,組成現成的東西而不產生開銷,你可以使用具有惰性范圍的范圍庫,例如Boost.Range ,例如:

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

正如@hvd 在評論中指出的那樣, transform_if double 導致不同的類型(在這種情況下是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 On Coliru

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

很抱歉在這么長時間后重新提出這個問題。 我最近也有類似的需求。 我通過編寫一個采用 boost::optional 的 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 };
          });

新的 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”,並且可能必須創建 lambda 以使它們“適合”算法。 雖然如果邏輯很復雜,將關注點分開是很好的,但如果只是從輸入類型中提取成員並檢查其值或將其添加到集合中,它再次比使用算法簡單得多。

另外,一旦你添加了某種transform_if,你必須決定是在transform之前還是之后應用謂詞,或者甚至有2個謂詞並在兩個地方應用它。

那么我們要做什么呢? 添加3個算法? (並且在編譯器可以在轉換的任一端應用謂詞的情況下,用戶很容易錯誤地選擇錯誤的算法並且代碼仍然編譯但產生錯誤的結果)。

此外,如果集合很大,用戶是否想要使用迭代器或映射/減少循環? 隨着 map/reduce 的引入,您會在等式中變得更加復雜。

從本質上講,該庫提供了工具,而用戶則留在這里使用它們來適應他們想做的事情,而不是像算法那樣經常出現這種情況。 (看看上面的用戶如何嘗試使用累積來扭曲事物以適合他們真正想做的事情)。

舉個簡單的例子,一張地圖。 對於每個元素,如果鍵是偶數,我將輸出值。

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 算法中嗎?

一段時間后再次找到這個問題,並設計了一整套可能有用的通用迭代器適配器后,我意識到原來的問題只需要std::reference_wrapper

使用它而不是指針,你很好:

住在 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 

該標准的設計方式是盡量減少重復。

在這種特殊情況下,您可以使用簡單的 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;
}

您可以同時使用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;
}
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
    }
);

這只是對問題 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;
}

請注意,我剛剛在這里用 C++ 實現了Rust 的方法

您可以使用std::accumulate對指向目標容器的指針進行操作:

住在 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 

C++20 帶來了范圍,並帶來了一組新的算法來操作它們。 此添加中最強大的工具之一是視圖

  • 它們支持惰性求值,這意味着元素是根據請求而不是在構造時生成的。 因此性能考慮被擱置(原始問題提到如何創建具有中間結果的臨時向量是次優的)。
  • 它們是可組合的,這意味着操作可以輕松鏈接在一起,而不會損失性能或表現力。

借助這些新工具,將 if 操作轉換為:

  • “使用函數A變換向量v
  • 僅當元素滿足條件B

變得如此簡單:

v | std::views::filter(B) | std::views::transform(A)

現在可以公平地說,有一種非常直接的方法可以使用標准庫進行“轉換 if”。

原來問的可以寫成:

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

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

    auto less4 =  [](ha const& h) { return h.i < 4; };
    auto pnter finally=  [](ha const& h) { return std::addressof(h); };
 
    for (auto vp : v | std::views::filter(less4) 
                     | std::views::transform(pnter)) 
    {
        std::cout << vp->i << ' ';
    }    
}

演示

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM