为什么C++标准库中没有transform_if?

Nik*_*iou 72 c++ c++-standard-library stl-algorithm

当想要执行一个副本(1. doable with 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;
}
Run Code Online (Sandbox Code Playgroud)

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.
Run Code Online (Sandbox Code Playgroud)
  1. 使用可用的C++标准库工具是否有更优雅的解决方法?
  2. transform_if库中没有原因吗?现有工具的组合是否具有足够的解决方案和/或被认为是性能良好的表现?

seh*_*ehe 33

标准库有利于基本算法.

如果可能,容器和算法应该彼此独立.

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

如果你需要转换if,你可以轻而易举地写它.如果你想要它/ today /,组成现成品并且不会产生开销,你可以使用具有惰性范围的范围库,例如Boost.Range,例如:

v | filtered(arg1 % 2) | transformed(arg1 * arg1 / 7.0)
Run Code Online (Sandbox Code Playgroud)

正如@hvd在注释中指出的那样,transform_ifdouble会产生不同的类型(double在本例中).组成顺序很重要,使用Boost Range你也可以写:

 v | transformed(arg1 * arg1 / 7.0) | filtered(arg1 < 2.0)
Run Code Online (Sandbox Code Playgroud)

导致不同的语义.这促成了这一点:

这让很少的意义,包括std::filter_and_transform,std::transform_and_filter,std::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"));
}
Run Code Online (Sandbox Code Playgroud)

  • 嗯,问题是标准算法不能轻易编写,因为它们不是懒惰的. (19认同)
  • @JanHudec 确实如此。(对于那个很抱歉? :))。这就是您使用库的原因(很像您使用 AMP/TBB 来实现并发,或者使用 C# 中的响应式扩展)。许多人正在研究范围提议+实施以纳入标准。 (2认同)
  • @sehe +1非常令人印象深刻,我今天学到了新东西!你是否善意告诉我们谁不熟悉Boost.Range和Phoenix我们可以在哪里找到解释如何使用`boost :: phoenix`来制作没有lambdas的好谓词的文档/示例?快速谷歌搜索返回没有任何相关性.谢谢! (2认同)
  • 我不同意“包含 std::filter_and_transform 没有什么意义”部分。其他编程语言也在其“标准库”中提供这种组合。迭代元素列表一次,动态转换它们,同时跳过那些无法转换的元素是完全有意义的。其他方法需要不止一次通过。是的,你可以使用BOOST,但问题实际上是“为什么C++标准库中没有transform_if?”。恕我直言,他的质疑是正确的。标准库中应该有这样的函数。 (2认同)
  • @sehe 关于“他们都使用可组合抽象”:这不是真的。例如,Rust 就具有这样的“transform_if”。它称为 [`filter_map`](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.filter_map)。然而,我必须承认它是为了简化代码,但另一方面,可以在 C++ 情况下应用相同的参数。 (2认同)

Ric*_*ges 7

很抱歉在这么长时间后重新提出这个问题。我最近也有类似的需求。我通过编写一个采用 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);
}
Run Code Online (Sandbox Code Playgroud)

像这样使用:

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 };
          });
Run Code Online (Sandbox Code Playgroud)


Cas*_*Cow 5

新的for循环符号在很多方面减少了对访问集合中每个元素的算法的需求,现在它更简洁,只需编写循环并将逻辑放在原位.

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

现在放入算法真的提供了很多价值吗?虽然是,但算法对C++ 03有用,实际上我有一个用于它,我们现在不需要一个,所以添加它没有真正的优势.

请注意,在实际使用中,您的代码看起来并不总是如此:您不一定具有"op"和"pred"功能,并且可能必须创建lambdas以使它们"适合"算法.虽然如果逻辑是复杂的,分离问题是很好的,如果只是从输入类型中提取成员并检查其值或将其添加到集合中,那么再次使用算法要简单得多.

此外,一旦添加某种transform_if,就必须决定是在变换之前还是之后应用谓词,或者甚至有两个谓词并在两个地方都应用它.

那我们该怎么办?添加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;
}         
Run Code Online (Sandbox Code Playgroud)

很好,很简单.是否适合transform_if算法?

  • 如果你认为上面的代码比带有2个lambdas的transform_if有更多的错误空间,一个用于谓词,一个用于转换,那么请解释一下.汇编,C和C++是不同的语言,有不同的地方.算法可能优于循环的唯一地方是"映射/减少"的能力,因此在大型集合上同时运行.然而,这样用户可以控制是按顺序循环还是map-reduce. (3认同)
  • 在适当的函数方法中,谓词和增变器的函数是明确定义的块,这使得构造适当地构造.对于循环体,其中可以包含任意内容,并且必须仔细分析您看到的每个循环以了解其行为. (3认同)
  • "把它融入transform_if算法?"*是*"transform_if算法",除了它具有硬编码的所有内容. (3认同)
  • 为适当的功能语言保留适当的功能方法。这是C ++。 (2认同)
  • 它执行与transform_if 等效的操作。只是算法应该以某种方式简化您的代码或改进它,而不是使其更复杂。 (2认同)

seh*_*ehe 5

一段时间后再次找到这个问题,并设计了一整套可能有用的通用迭代器适配器后,我意识到原来的问题只需要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 << " ";
}
Run Code Online (Sandbox Code Playgroud)

印刷

1 1 
Run Code Online (Sandbox Code Playgroud)


Nik*_*iou 5

C++20 带来了范围以及一组新的算法来对其进行操作。此添加中最强大的工具之一是视图

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

借助这些新工具,转换 if 操作可以:

  • v“使用函数变换向量A
  • 仅当元素满足条件时B

变得简单如下:

v | std::views::filter(B) | std::views::transform(A)
Run Code Online (Sandbox Code Playgroud)

现在可以公平地说,有一种非常直接的方法可以使用标准库进行“转换 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 =  [](ha const& h) { return std::addressof(h); };
 
    for (auto vp : v | std::views::filter(less4) 
                     | std::views::transform(pnter)) 
    {
        std::cout << vp->i << ' ';
    }    
}
Run Code Online (Sandbox Code Playgroud)

演示