13

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

Barry
  • 286,269
  • 29
  • 621
  • 977
arynaq
  • 6,710
  • 9
  • 44
  • 74
  • 3
    Related: https://stackoverflow.com/q/23579832/1896169 . There is no `std::transform_if`, but if you are willing to use libraries, there are several libraries that can do this – Justin Jun 18 '18 at 17:30
  • 1
    Of course, `std::copy_if` will work just fine if the source type can be assigned to the target type. – Pete Becker Jun 18 '18 at 18:46
  • @PeteBecker: With the proviso that if the assignment gives a warning, you'll get a template instantiation stack in your warning message. VS2017 produces about 50 lines for an int-to-float conversion. The "extraction function" could be as simple as `static_cast( )`. – MSalters Jun 21 '18 at 15:41
  • @MSalters -- yes, some compilers are horrible to work with. Especially when they decide that they know your requirements better than you do. Turn off stupid warnings! – Pete Becker Jun 21 '18 at 16:52

6 Answers6

13

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.

Barry
  • 286,269
  • 29
  • 621
  • 977
7

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;
}
Xirema
  • 19,889
  • 4
  • 32
  • 68
2

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

Justin
  • 24,288
  • 12
  • 92
  • 142
1

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

YSC
  • 38,212
  • 9
  • 96
  • 149
0

This calls for a hypothetical std::transform_if, which is not available (why?).

A somewhat expensive work-around would be to std::copy_if into a temporary vector, followed by a 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;

Demo.

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
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;
}
isnullxbh
  • 807
  • 13
  • 20