使用不同类型的copy_if函数

13

如果我知道如何提取匹配类型,是否有一种现代的表达方式来表示有条件地从不同类型的源容器复制到目标容器?

更容易的方法是通过代码示例提出问题:

#include <algorithm>
#include <vector>

struct Foo {};
struct FooBar{
    bool is_valid;
    Foo foo;
};

std::vector<Foo> get_valid_foos(const std::vector<FooBar>& foobars){
    std::vector<Foo> valid_foos;
    for(const auto& fbar : foobars){
        if(fbar.is_valid)
            valid_foos.push_back(fbar.foo);
    }
    return valid_foos;
}

std::vector<Foo> get_valid_foos_modern(const std::vector<FooBar>& foobars){
    std::vector<Foo> valid_foos;
    std::copy_if(foobars.begin(), foobars.end(), std::back_inserter(valid_foos),
        [](const auto& foobar){
            return foobar.is_valid;
        });
    //?? std::copy requires input and output types to match
    return valid_foos;
}

https://godbolt.org/g/miPbfW


3
相关链接:https://dev59.com/nGAg5IYBdhLWcg3wb6od 。虽然没有std :: transform_if,但如果您愿意使用库,有几个库可以实现此功能。 - Justin
1
当然,如果源类型可以分配给目标类型,std::copy_if 将正常工作。 - Pete Becker
@PeteBecker:有一个前提条件,如果赋值语句出现警告,你将在警告信息中得到一个模板实例化堆栈。VS2017为int-to-float转换生成大约50行代码。 "提取函数"可以简单地使用static_cast<float>() - MSalters
@MSalters -- 是的,有些编译器非常难以使用。特别是当它们认为自己比你更了解你的需求时。关闭愚蠢的警告! - Pete Becker
6个回答

13

使用range-v3

std::vector<Foo> get_valid_foos(const std::vector<FooBar>& foobars) {
    return foobars
        | view::filter(&FooBar::is_valid)
        | view::transform(&FooBar::foo);
}

这很表达性。


1
@arynaq,有关添加类似内容的工作正在进行中。请参阅Ranges TS - Justin
1
如果您使用vcpkg,则支持有限版本的Range V3。 - Justin
3
很可能许多Ranges TS的特性都会包含在C++20标准中。 - Barry
嗯,既然你在这里,为什么不将它作为std::span传递呢?这也应该是C++20的一部分。 - Deduplicator
指南支持库(也包含span)中是否有任何部分在C++11以上不可用? - Deduplicator
显示剩余4条评论

7
像其他回答所提出的那样,范围(Ranges)提供了这个问题的非常简洁的解决方案。不过我们还需要几年时间才能使C++20标准化(并且在企业环境中使用之前还需要更多年),因此我们需要一个兼容C++17的解决方案。
你要寻找的是一个假设的transform_if,在各种原因下未包含在标准库中。
你有几个选择。
最简单的方法是将std::copy_if和std::transform组合起来使用。
std::vector<Foo> get_valid_foos_modern(const std::vector<FooBar>& foobars){
    std::vector<FooBar> valid_foobars;
    std::copy_if(foobars.begin(), foobars.end(), std::back_inserter(valid_foobars), [](const auto& foobar){
        return foobar.is_valid;
    });
    std::vector<Foo> valid_foos;
    std::transform(valid_foobars.begin(), valid_foobars.end(), std::back_inserter(valid_foos), [](auto const& fooBar) {return fooBar.foo;});
    return valid_foos;
}

这种方法的缺点是它会为每个要进行转换的对象创建临时的FooBar对象,这可能会让你感到不满意。你可以自己编写transform_if算法的实现:
template<typename InputIterator, typename OutputIterator, typename Predicate, typename TransformFunc>
OutputIterator transform_if(
    InputIterator&& begin, 
    InputIterator&& end, 
    OutputIterator&& out, 
    Predicate&& predicate, 
    TransformFunc&& transformer
) {
    for(; begin != end; ++begin, ++out) {
        if(predicate(*begin))
            *out = transformer(*begin);
    }
    return out;
}

然后你就可以直接在你的代码中使用它:

std::vector<Foo> get_valid_foos_modern(const std::vector<FooBar>& foobars){
    std::vector<Foo> valid_foos;
    transform_if(
        foobars.begin(), 
        foobars.end(), 
        std::back_inserter(valid_foos), 
        [](const auto& foobar) { return foobar.is_valid;},
        [](auto const& foobar) { return foobar.foo;}
    );
    return valid_foos;
}

2
尽管不如range-v3那么好用,但您可以使用Boost Range进行操作:Boost Range
std::vector<Foo> get_valid_foos(const std::vector<FooBar>& foobars) {
    std::vector<Foo> result;

    boost::push_back(
        result, foobars | boost::adaptors::filtered([](const FooBar& foobar) {
                    return foobar.is_valid;
                }) | boost::adaptors::transformed([](const FooBar& foobar) {
                    return foobar.foo;
                }));

    return result;
}

Demo


1
一个后插入迭代器it将尝试将任何分配给it的内容push_back。 目前,您会因为it = foobar而出现错误。实际上,vector_of_foo.push_back(foobar)本身就是不符合规范的。
如果只有一种方法可以将FooBar隐式转换为Foo ... 等等!确实有!烦人的是它在FooFooBar之间引入了循环依赖关系。让我们用CRTP打破它!
template<class TFoo>
struct TFooBar
{
    bool is_valid;
    TFoo foo;
};
struct Foo
{
    Foo() = default;
    Foo(TFooBar<Foo> const& src) { *this = src.foo; }
};
using FooBar = TFooBar<Foo>;

现在,std::back_inserter(foos) = FooBar{} 的效果符合预期。而 copy_if 也会按照预期运行!
auto get_valid_foos_modern(const std::vector<FooBar>& foobars){
    std::vector<Foo> result;
    std::copy_if(begin(foobars), end(foobars), std::back_inserter(result),
        [](const auto& foobar) {
            return foobar.is_valid;
    });
    return result;
}

演示:http://coliru.stacked-crooked.com/a/a40aeca7a9a057b2


0

这需要一个假想的std::transform_if,但它不可用(为什么?)。

一个有点昂贵的解决方法是使用std::copy_if复制到临时向量中,然后使用std::transform

std::vector<FooBar> foobars_with_valid_foos;
std::copy_if(
    foobars.begin()
,   foobars.end()
,   std::back_inserter(foobars_with_valid_foos)
,   [](const auto& foobar){
        return foobar.is_valid;
    }
);
std::vector<Foo> valid_foos;
std::transform(
    foobars_with_valid_foos.begin()
,   foobars_with_valid_foos.end()
,   std::back_inserter(valid_foos)
,   [](const auto& foobar){
        return foobar.foo;
    }
);
return valid_foos;

演示。


我之前从未见过这种格式化风格...在函数调用的行首出现, 是我前所未见的。 - Justin
2
@Justin 这种风格是从 SQL 中借鉴来的。 - Sergey Kalinichenko
临时解决方案较慢。 - Andrei R.
@AndreiR。比什么慢?页面上的另外四个解决方案中有两个使用了临时变量。 - Sergey Kalinichenko
@dasblinkenlight,我认为更简单的代码能够更快地运行,这被称为“优雅的解决方案”,而不是“过早的优化”。 - Andrei R.
显示剩余3条评论

0
#include <iterator>
#include <functional>
#include <vector>
#include <iostream>

template<typename Container,
         typename In>
class MappedInsertIterator
    : public std::back_insert_iterator<Container>
{
protected:
    using Out = typename Container::value_type;
    using Transformer = std::function<Out(const In&)>;

public:
    MappedInsertIterator() = delete;

    template<typename F>
        requires std::is_invocable_r_v<Out, F, In>
    explicit MappedInsertIterator(Container& c, F&& fn);

    virtual ~MappedInsertIterator() = default;

public:
    auto operator*() -> MappedInsertIterator&;
    auto operator=(const auto& value) -> MappedInsertIterator&;

protected:
    Transformer m_fn;
};

template<typename Container, typename In>
template<typename F>
    requires std::is_invocable_r_v<typename Container::value_type, F, In>
inline MappedInsertIterator<Container, In>::MappedInsertIterator(Container& c, F&& fn)
    : std::back_insert_iterator<Container>(c)
    , m_fn(std::forward<F>(fn))
{}

template<typename Container, typename In>
inline auto MappedInsertIterator<Container, In>::operator*() -> MappedInsertIterator&
{ return *this; }

template<typename Container, typename In>
auto MappedInsertIterator<Container, In>::operator=(const auto& value) -> MappedInsertIterator&
{
    std::back_insert_iterator<Container>::operator=(m_fn(value));
    return *this;
}


int main()
{
    struct Telemetry { float voltage; unsigned timestamp; };

    std::vector<Telemetry> items =
        {
            Telemetry { .voltage = 200, .timestamp = 101 }, // accepted
            Telemetry { .voltage = 250, .timestamp = 102 }, // accepted
            Telemetry { .voltage = 300, .timestamp = 203 }, // rejected
        };

    static auto predicate = [](const Telemetry& t){ return t.timestamp < 200; };
    static auto transform = [](const Telemetry& t){ return t.voltage; };

    std::vector<float> voltages;
    using iterator_t = MappedInsertIterator<decltype(voltages), Telemetry>;
    std::copy_if(items.cbegin(), items.cend(), iterator_t(voltages, transform), predicate);

    for (const auto& v : voltages)
        std::cout << v << std::endl;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接