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


82

想要进行有条件的复制时出现一个用例(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;
}

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
奥利弗·查尔斯沃思

@OliCharlesworth实际上copy_if还暗示“仅在满足特定条件时才复制”。同样是模棱两可的。
Shahbaz 2014年

@Shahbaz:那是什么copy_if,对吗?
奥利弗·查尔斯沃思

2
如果对这样的事情的名字的争执是不实施它的真正原因,我将不会感到惊讶!
Nikos Athanasiou 2014年

6
也许我在这些注释中丢失了一些内容,但是transform_if如果转换可以是不同的不兼容类型,那么如何复制那些不会转换的元素呢?问题中的实现恰恰是我期望该功能实现的目的。

Answers:


32

标准库支持基本算法。

容器和算法应尽可能彼此独立。

同样,作为速记,很少包含可以由现有算法组成的算法。

如果需要转换,则可以简单地编写它。如果您希望/ today /组成现成产品而不产生开销,则可以使用具有惰性范围的范围库,例如Boost.Range,例如:

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

正如@hvd在评论中指出的那样,transform_if双精度会导致使用不同的类型(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"));
}

26
好吧,问题在于标准算法不容易编写,因为它们并不懒惰。
Jan Hudec

1
@JanHudec确实。(对于那个很抱歉? :))。这就是为什么使用库的原因(就像您将AMP / TBB用于并发,或C#中的反应性扩展一样)。许多人正在研究范围主张和实施方案,以纳入标准。
sehe 2014年

2
@sehe +1非常令人印象深刻,我今天学到了一些新东西!您是否愿意告诉我们不熟悉Boost.Range和Phoenix的人,在哪里可以找到说明如何使用boost::phoenix无lambda的良好谓词的文档/示例?快速的Google搜索未返回任何相关信息。谢谢!
阿里

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”,并且可能必须创建lambda以使其“适合”算法。如果逻辑很复杂,最好将关注点分离出来,如果只是从输入类型中提取成员并检查其值或将其添加到集合中,那么比使用算法要简单得多。

另外,一旦添加某种transform_if,就必须决定是在转换之前还是之后应用谓词,或者甚至拥有两个谓词并在两个位置都应用它。

那我们该怎么办?添加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算法中吗?


3
如果您认为上面的代码比带有2个lambda,一个用于谓词和一个用于transform的transform_if的错误空间还多,请解释一下。汇编,C和C ++是不同的语言,并且具有不同的位置。该算法唯一优于循环的地方是能够“映射/减少”从而在大型集合上同时运行的能力。但是,通过这种方式,用户可以控制是按顺序循环还是减少映射。
CashCow 2015年

3
在适当的功能方法中,谓词和mutator的功能是定义明确的块,这些块使构造具有正确的结构。因为循环主体中可以包含任意内容,所以必须仔细分析您看到的每个循环以了解其行为。
Bartek Banachewicz 2015年

2
为适当的功能语言保留适当的功能方法。这是C ++。
CashCow

3
“想把它适合到transform_if算法中吗?” 那“ transform_if算法”,除了它所有内容都经过硬编码。
R. Martinho Fernandes 2015年

2
它等效于transform_if。只是算法应该简化或以某种方式改进代码,而不是使其变得更复杂。
CashCow

5

很抱歉,过了很久才复活这个问题。最近我有类似的要求。我通过编写一个使用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 };
          });

1
没有测量-直到用户抱怨他们的经验受CPU限制(即从不),我才比纳秒更关心正确性。但是我看不到它很穷。可选参数非常便宜,因为没有内存分配,并且仅当实际填充了可选参数时才调用Ts构造函数。我希望优化器消除几乎所有无效代码,因为所有代码路径在编译时都是可见的。
理查德·霍奇斯

是的 我同意这是否与通用算法(实际上是那些通用算法)无关。除非有那么简单的东西,否则在这里我通常不会被吸引。此外,我希望将可选处理作为任何输出迭代器上的装饰器(因此,至少在尝试弥补算法可组合性不足的同时,至少我们可以获得输出迭代器的可组合性)。
sehe 2015年

从逻辑上来说,您是通过迭代器上的装饰器还是在转换函数中处理可选插入,这没有什么区别。最终,这仅仅是对标志的测试。我认为您会发现优化的代码在任何一种方式上都是相同的。完全优化的唯一障碍是异常处理。将T标记为具有noexcept构造函数可以解决此问题。
理查德·霍奇斯

您希望对transform()采取哪种形式?我确信我们可以构建一个可组合的迭代器套件。
Richard Hodges 2015年

我也是:)我在评论你的建议。我没有提出其他建议(我很早以前就提出过。让我们改为使用范围和可组合算法:))
sehe

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

3
“为什么不?” -因为代码是针对人类的。对我来说,摩擦实际上比回到编写函数对象而不是lambda更糟。*static_cast< back_insert_iterator<vector<ha *>> &>(*this) = &arg;是既不可读又不必要的具体。请参阅此c ++ 17用法,了解更多常规用法。
sehe

这是一个不对基本迭代器进行硬编码的版本(因此您可以将其与std::insert_iterator<>or一起使用std::ostream_iterator<>),还可以提供一个转换(例如作为lambda)。C ++ 17,开始寻找有用/在C ++相同11
sehe

请注意,此时,没有什么理由保留基本迭代器,您可以简单地:使用任何函数,注意Boost包含更好的实现:boost :: function_output_iterator。现在剩下的就是重新发明for_each_if:)
sehe

实际上,重新阅读原始问题,让我们添加一个理由-仅使用c ++ 11标准库。
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;
}

用法:(请注意,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

0

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

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

观看科利鲁直播

#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对指向目标容器的指针进行操作的:

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 
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.