为啥C++标准库中没有transform_if?

Posted

技术标签:

【中文标题】为啥C++标准库中没有transform_if?【英文标题】:Why is there no transform_if in the C++ standard library?为什么C++标准库中没有transform_if? 【发布时间】:2014-06-28 02:12:31 【问题描述】:

当想要进行连续复制时出现了一个用例(1. 可以使用copy_if)但从值容器到指向这些值的指针容器(2. 可以使用transform)。

使用可用的工具,我不能在不到两个步骤中do it:

#include <vector>
#include <algorithm>

using namespace std;

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

int main() 

    vector<ha> v ha1, ha7, ha1 ; // 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 并消除临时的需要,但更好的是,implement(对于一元操作)并不难:

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.
    使用可用的 C++ 标准库工具是否有更优雅的解决方法? 库中不存在transform_if 是否有原因?现有工具的组合是否是一种足够的解决方法和/或被认为表现良好?

【问题讨论】:

(IMO) 名称transform_if 暗示“仅在满足某个谓词时才进行转换”。您想要的更具描述性的名称是 copy_if_and_transform! @OliCharlesworth,实际上copy_if 还暗示“仅在满足某个谓词时才复制”。它同样模棱两可。 @Shahbaz:但这就是copy_if 所做的,对吧? 如果关于这种东西的名称的争议是不实施它的实际原因,我不会感到惊讶! 也许我在这些 cmets 中遗漏了一些东西,但如果转换可以是不同的不兼容类型,transform_if 怎么可能复制那些它不转换的元素?问题中的实现正是我期望这样一个函数做的事情。 【参考方案1】:

标准库偏爱基本算法。

如果可能,容器和算法应该相互独立。

同样,可以由现有算法组成的算法很少被包括在内,作为简写。

如果你需要一个转换 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"));

【讨论】:

嗯,问题是标准算法不容易组合,因为它们不懒。 @JanHudec 确实如此。 (对于那个很抱歉? :))。这就是您使用库的原因(很像您使用 AMP/TBB 进行并发,或使用 C# 中的反应式扩展)。许多人正在研究范围提议 + 实施以纳入标准。 @sehe +1 令人印象深刻,我今天学到了新东西!您能否告诉我们不熟悉 Boost.Range 和 Phoenix 的人在哪里可以找到解释如何使用 boost::phoenix 在没有 lambda 的情况下创建如此好的谓词的文档/示例?快速的谷歌搜索没有返回任何相关内容。谢谢! 我不同意“包含 std::filter_and_transform”部分。其他编程语言也在其“标准库”中提供这种组合。遍历一个元素列表一次,即时转换它们,同时跳过那些无法转换的元素,这是完全有意义的。其他方法需要不止一次通过。是的,您可以使用 BOOST,但问题实际上是“为什么 C++ 标准库中没有 transform_if?”。恕我直言,他对此提出质疑是正确的。标准库里应该有这样的功能。 @sehe 关于“它们都使用可组合的抽象”:那不是真的。例如,Rust 正好有这样的transform_if。它被称为filter_map。但是,我必须承认它的存在是为了简化代码,但另一方面,可以在 C++ 案例中应用相同的参数。【参考方案2】:

很抱歉这么久才重新提出这个问题。我最近有类似的要求。我通过编写一个带有 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 ;
          );

【讨论】:

未测量 - 直到用户抱怨他们的体验受 CPU 限制(即从不)我更关心正确性而不是纳秒。但是我看不出它很穷。选项非常便宜,因为没有内存分配,并且只有在实际填充了选项时才调用 Ts 构造函数。我希望优化器消除几乎所有死代码,因为所有代码路径在编译时都是可见的。 是的。如果它不完全是关于通用算法(实际上是其中的通用构建块),我会同意。这是我通常不感兴趣的地方,除非事情很简单。此外,我希望可选处理成为任何输出迭代器上的装饰器(所以至少我们获得了输出迭代器的可组合性,同时我们试图弥补算法缺乏可组合性)。 无论您是通过迭代器上的装饰器还是在转换函数中处理可选插入,在逻辑上没有区别。这最终只是对一面旗帜的考验。我想你会发现优化后的代码无论如何都是一样的。唯一阻碍完全优化的就是异常处理。将 T 标记为具有 noexcept 构造函数可以解决此问题。 您希望对 transform() 的调用采用什么形式?我确信我们可以构建一个可组合的迭代器套件。 这很方便!如果将 boost 替换为 std::optional,它也适用于 c++17【参考方案3】:

新的 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 进行循环?随着 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 算法中吗?

【讨论】:

如果你认为我上面的代码比带有 2 个 lambdas 的 transform_if 有更多的错误空间,一个用于谓词,一个用于转换,那么请解释一下。汇编、C 和 C++ 是不同的语言,有不同的地方。该算法可能优于循环的唯一地方是“映射/减少”的能力,因此可以在大型集合上同时运行。然而,这样用户可以控制是按顺序循环还是 map-reduce。 在适当的函数式方法中,谓词和增变器的函数是定义良好的块,可以使构造正确结构化。 For 循环体中可以包含任意内容,您看到的每个循环都必须仔细分析以了解其行为。 为正确的函数式语言保留正确的函数式方法。这是 C++。 “想把它融入到 transform_if 算法中吗?”那一个“transform_if算法”,除了它所有的东西都是硬编码的。 它执行相当于一个 transform_if。只是算法应该简化你的代码或以某种方式改进它,而不是让它变得更复杂。【参考方案4】:

一段时间后再次找到这个问题,devising a whole slew of potentially useful generic iterator adaptors 我意识到原来的问题只需要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 

【讨论】:

【参考方案5】:

该标准的设计目的是尽量减少重复。

在这种特殊情况下,您可以通过简单的 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 ha1, ha7, ha1 ; // 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;

【讨论】:

【参考方案6】:

您可以同时使用copy_if为什么不呢?定义OutputIt(参见copy):

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 ha1, ha7, ha1 ; // 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;

【讨论】:

“为什么不呢?” - 因为代码是为人类服务的。对我来说,摩擦实际上比回到编写函数对象而不是 lambdas 更糟糕。 *static_cast&lt; back_insert_iterator&lt;vector&lt;ha *&gt;&gt; &amp;&gt;(*this) = &amp;arg; 既不可读又不必要的具体。请参阅此c++17 获取更多通用用法。 这是一个没有硬编码基本迭代器的版本(因此您可以将它与 std::insert_iterator&lt;&gt;std::ostream_iterator&lt;&gt; 一起使用,例如),还可以让您提供转换(例如作为 lambda)。 c++17, Starting to look useful/Same in c++11 注意,此时,没有理由保留基迭代器,您可以简单地:use any function,注意 Boost 包含更好的实现:boost::function_output_iterator。现在剩下的就是重新发明for_each_if :) 其实重读原题,我们加个voice of reason——只用c++11标准库。【参考方案7】:

C++20 带来了ranges 和一组新的algorithms 来对它们进行操作。此添加中最强大的工具之一是views

它们支持惰性求值,这意味着元素是根据请求而不是构造生成的。因此,性能方面的考虑被搁置了(最初的问题提到了如何创建具有中间结果的临时向量是次优的)。 它们是可组合的,这意味着可以轻松地将操作链接在一起而不会损失性能或表现力。

有了这些新工具,转换 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 ha1, ha7, ha1, ha4, ha3, ha0 ;

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

Demo

【讨论】:

【参考方案8】:
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 a1, 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
    
);

【讨论】:

您是否认为该实施已准备好生产?它适用于不可复制的元素吗?还是移动迭代器?【参考方案9】:

这只是对问题 1“可用 C++ 标准库工具有更优雅的解决方法吗?”的回答。

如果您可以使用 c++17,那么您可以使用 std::optional 以获得仅使用 C++ 标准库功能的更简单的解决方案。这个想法是在没有映射的情况下返回std::nullopt

See live on 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 ha1, ha7, ha1 ; // 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's approach。

【讨论】:

【参考方案10】:

您可以使用std::accumulate,它对指向目标容器的指针进行操作:

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 

【讨论】:

以上是关于为啥C++标准库中没有transform_if?的主要内容,如果未能解决你的问题,请参考以下文章

为啥标准 C++ 库中没有“int pow(int base, int exponent)”?

为啥 C++ 标准库中没有 SIMD 功能?

c++标准库中没有关于正则匹配字符串的函数么

为啥 Python 的标准库中没有排序容器?

为啥 Cdecl 调用在“标准”P/Invoke 约定中经常不匹配?

为啥 Cdecl 调用在“标准”P/Invoke 约定中经常不匹配?