Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

copy_if with different types

Tags:

c++

c++14

Is there a modern way of expressing the intent to conditionally copy from a source container of a different type to a destination container if I know how to extract the matching type?

It is easier to pose the question as a code-example:

#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

like image 475
arynaq Avatar asked Jun 18 '18 17:06

arynaq


People also ask

What does Copy_if return?

Returns an iterator pointing to the element that follows the last element written in the result sequence.

What does std copy do?

std::copy. Copies the elements in the range [first,last) into the range beginning at result . The function returns an iterator to the end of the destination range (which points to the element following the last element copied).


4 Answers

Like the other answer put forth, Ranges offer a very concise solution to this problem. We're still a few years out from C++20 being standardized though (and another few years before it becomes accessible in enterprise environments) so we need a C++17-compatible solution.

What you're looking for is a hypothetical transform_if, which was not included in the Standard Library for various reasons

You have a couple of options.

The simplest is to just combine std::copy_if and 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;
}

The downside to this approach is that it creates temporary FooBar objects for each object that is going to get transformed, which you may find undesirable. You could roll your own transform_if algorithm implementation:

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

Which you'd then be able to use directly in your code:

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;
}
like image 89
Xirema Avatar answered Oct 25 '22 09:10

Xirema


Using range-v3:

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

That's pretty expressive.

like image 42
Barry Avatar answered Oct 25 '22 09:10

Barry


Although not as nice as range-v3, you could use 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

like image 32
Justin Avatar answered Oct 25 '22 07:10

Justin


A back insertor iterator it will try and push_back anything that is assigned to it. Currently, you get an error because it = foobar is ill-formed. Indeed vector_of_foo.push_back(foobar) is ill-formed itself.

If only there was a way to implicitly convert a FooBar into a Foo... wait! There is! Well, the annoying thing is that it introduces a circular dependency between Foo and FooBar. Let us break it with 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>;

Now, std::back_inserter(foos) = FooBar{} does what is expected. And copy_if will behave too!

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

Demo: http://coliru.stacked-crooked.com/a/a40aeca7a9a057b2

like image 26
YSC Avatar answered Oct 25 '22 09:10

YSC