-1

I have a mathy library. In this library, I have functions to manipulate hyperplanes in a simplex space, so that I can sort through them in various ways.

It turns out that these hyperplanes can represent different things in different contexts. While the math is the same, in each context the hyperplanes mean a different thing, and are associated with different data structures.

It is advantageous to me to be able to write the code to manipulate the hyperplanes once, but allowing them to handle different data structures.

Below is a simplified example that tries to explain what I am trying to do:

// Assume this struct represent my hyperplane, or whatever
// construct I want to be able to manipulate.
struct Math {
    int x;
};

// Here is my function `foo` which expects a range of Math. It does
// some work on it, and re-arranges it as it is useful to me.
template <typename It>
void foo(It begin, It end) {
    while (begin < end) {
        --end;
        if (begin->x < end->x)
            std::iter_swap(begin, end);
        ++begin;
    }
}
template <typename Range>
void foo(Range & r) {
    foo(ranges::begin(r), ranges::end(r));
}

This is basically my underlying functionality, which is common for every additional class that uses my hyperplanes (or, in this case, the Math class).

Now in other parts of my library I have classes that look like this:

struct Something {
    int additional_metadata;
    Math contextual_name_for_math;
};

struct SomethingElse {
    double other_metadata;
    std::vector<int> some_other_metadata;
    Math another_different_contextual_name;
};

Now I need to be able to apply foo to ranges of these classes and rearrange them based on the properties of the Math they contain. At the same time:

  • foo does not know the contextual name that Math has in each of these classes.
  • foo does not care about the additional metadata that is present.

What I would like to write is something like this:

// Create data
std::vector<Something> S{{1,{2}},{3,{4}},{5,{6}}};

// Transform data in to view of Math, so that 'foo' can work on it
auto S_as_math = S | ranges::view::transform(
    // I guess I can remove the consts here, although `foo` does not 
    // really need to alter the values, it only shuffles things around.
    [](auto const& s) -> Math const& { return s.contextual_name_for_math; }
);

// Do work inline on the view, but hopefully on the underlying S too.
foo(S_as_math);

// Print results.
for (auto s : S) {
    std::cout << '(' << s.additional_metadata << ", " 
                     << s.contextual_name_for_math.x << ")\n";
}
std::cout << "\n";

// expected, keeps Math and associated metadata together:
//
// (5, 6)
// (3, 4)
// (1, 2)
//
// NOT WANTED, only shuffles Math with no regard for metadata:
//
// (1, 6)
// (3, 4)
// (5, 2)

Currently I am doing this by passing boost::transform_iterators to foo that extract the Math component when dereferenced, and by using a custom implementation of iter_swap inside foo which is able to know whether it is being passed a proxy iterator and always swaps the underlying originals. This achieves what I want.

I am curious whether this is possible to do using ranges-v3. Currently I am able to compile this example if I remove the consts in the lambda I use to unwrap the Something class, but then foo only shuffles the Maths without keeping them together with their metadata.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
Svalorzen
  • 5,353
  • 3
  • 30
  • 54

1 Answers1

0

Pass your transformation function to foo, don't foo the transformed range.

template <typename It, typename UnaryFunction>
void foo(It begin, It end, UnaryFunction func) {
    while (begin < end) {
        --end;
        if (func(*begin).x < func(*end).x)
            std::iter_swap(begin, end);
        ++begin;
    }
}
template <typename Range, typename UnaryFunction>
void foo(Range & r, UnaryFunction func) {
    foo(ranges::begin(r), ranges::end(r));
}

int main()
{
    std::vector<Something> S{{1,{2}},{3,{4}},{5,{6}}};
    auto S_as_math = [](auto const& s) { return s.contextual_name_for_math; };
    foo(S, S_as_math);

    for (auto s : S) {
        std::cout << '(' << s.additional_metadata << ", " 
                         << s.contextual_name_for_math.x << ")\n";
    }
    std::cout << "\n";   
}

You can keep your original template or default UnaryFunction to an identity function if you are using purely Math ranges.

Caleth
  • 52,200
  • 2
  • 44
  • 75
  • I could do that, but then this does not really benefit from the `ranges` library, does it? I also don't like this solution as much - to me it's much cleaner to keep the complexity in the iterators, since it can be arbitrary. This would allow to keep signatures the same for all functions - be it my `foo`, or `std::sort` and so on, without forcing all of them to have a `func` parameter. Which I think is also kind of the goal of `ranges`? – Svalorzen Oct 14 '18 at 11:41
  • @Svalorzen A transformed range is a separate value to the source range. The other option to projecting in `foo`, you can require a comparator for each `Something` `bool math_less(const Something & lhs, const Something & rhs) { return lhs.contextual_name_for_math.x < rhs.contextual_name_for_math.x; }` – Caleth Oct 14 '18 at 12:50
  • I'm sorry, but I don't like this solution either. It assumes I'm only going to ever need to do a single specific operation in `foo`. I would need some kind of comparator for every possible operation, and at that point passing a unary transformation function would be easier. It also leads to lots of repeated code as all the comparators would be functionally exactly the same code. – Svalorzen Oct 18 '18 at 14:49
  • @Svalorzen you can write a `math_traits` templated over a unary transform. – Caleth Oct 18 '18 at 15:56